Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 519c973ffe | |||
| 600679a239 | |||
| f5bdc1bb58 |
2
.github/workflows/build-nightly.yml
vendored
2
.github/workflows/build-nightly.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
- name: Call common build workflow
|
||||
uses: ./.github/actions/common-docker-build
|
||||
with:
|
||||
build_tags: csmith1865/obscreen:nightly
|
||||
build_tags: jierka/obscreen:nightly
|
||||
manifest_tags: type=semver,pattern=nightly
|
||||
flavor: ""
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
||||
2
.github/workflows/build-pr.yml
vendored
2
.github/workflows/build-pr.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
- name: Call common build workflow
|
||||
uses: ./.github/actions/common-docker-build
|
||||
with:
|
||||
build_tags: csmith1865/obscreen:pr-${{ github.event.pull_request.number }}
|
||||
build_tags: jierka/obscreen:pr-${{ github.event.pull_request.number }}
|
||||
manifest_tags: type=semver,pattern=pr
|
||||
flavor: ""
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
||||
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
with:
|
||||
build_tags: |
|
||||
csmith1865/obscreen:v${{ steps.version.outputs.VERSION }}
|
||||
csmith1865/obscreen:latest
|
||||
csmith1865/obscreen:latest
|
||||
manifest_tags: type=semver,pattern=v${{ steps.version.outputs.VERSION }}
|
||||
flavor: latest=true
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -24,7 +24,4 @@ tmp.py
|
||||
/data/www/plugins/*
|
||||
!/data/www/plugins/.gitkeep
|
||||
/var/run/storage/*
|
||||
!/var/run/storage/.gitkeep
|
||||
*.egg-info
|
||||
/build/
|
||||
/dist/
|
||||
!/var/run/storage/.gitkeep
|
||||
@ -1,4 +0,0 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
docs/setup-run-on-rpi.md
|
||||
docs/setup-run-headless.md
|
||||
29
README.md
29
README.md
@ -6,9 +6,9 @@
|
||||
|
||||
Obscreen is a user-friendly self-hosted digital signage tool leveraging chromium browser.
|
||||
|
||||
<a target="_blank" href="https://git.sumisu.xyz/csmith1865/obscreen"><img src="https://img.shields.io/gitea/stars/csmith1865/obscreen?gitea_url=https%3A%2F%2Fgit.sumisu.xyz&style=flat" /></a> <a target="_blank" href="https://hub.docker.com/r/csmith1865/obscreen"><img src="https://img.shields.io/docker/pulls/csmith1865/obscreen" /></a> <a target="_blank" href="https://hub.docker.com/r/csmith1865/obscreen"><img src="https://img.shields.io/docker/v/csmith1865/obscreen/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://git.sumisu.xyz/csmith1865/obscreen"><img src="https://img.shields.io/gitea/last-commit/csmith1865/obscreen?gitea_url=https%3A%2F%2Fgit.sumisu.xyz&style=flat" /></a>
|
||||
<a target="_blank" href="https://github.com/jr-k/obscreen"><img src="https://img.shields.io/github/stars/jr-k/obscreen?style=flat" /></a> <a target="_blank" href="https://hub.docker.com/r/jierka/obscreen"><img src="https://img.shields.io/docker/pulls/jierka/obscreen" /></a> <a target="_blank" href="https://hub.docker.com/r/jierka/obscreen"><img src="https://img.shields.io/docker/v/jierka/obscreen/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/jr-k/obscreen"><img src="https://img.shields.io/github/last-commit/jr-k/obscreen" /></a>
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-playlist-edit.png" width="700" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-playlist-edit.png" width="700" alt="" />
|
||||
|
||||
🧑🎄 Open to feature request and pull request. [Cast your vote for your preferred ones on the Canny platform](https://obscreen.canny.io/feature-requests)
|
||||
|
||||
@ -34,7 +34,7 @@ It is a temporary live demo, all data will be deleted after 30 minutes (~30secs
|
||||
- Authentication management
|
||||
- Plays content from flashdrive in offline mode
|
||||
- Core API & Plugin system to extend capabilities
|
||||
- [Multi Languages](https://git.sumisu.xyz/csmith1865/obscreen/src/branch/master/lang)
|
||||
- [Multi Languages](https://github.com/jr-k/obscreen/tree/master/lang)
|
||||
- Cast pictures and iframes to Chromecast
|
||||
- No costly monthly pricing plan per screen or whatever, no cloud, no telemetry
|
||||
|
||||
@ -47,19 +47,19 @@ It is a temporary live demo, all data will be deleted after 30 minutes (~30secs
|
||||
|
||||
Light Mode:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-light-mode.png" width="512" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-light-mode.png" width="512" alt="" />
|
||||
|
||||
Content Explorer:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-content-explorer.png" width="512" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-content-explorer.png" width="512" alt="" />
|
||||
|
||||
Settings Page:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-settings.png" width="512" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-settings.png" width="512" alt="" />
|
||||
|
||||
Add Content Modal:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-add-content.png" width="512" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-add-content.png" width="512" alt="" />
|
||||
|
||||
## 🫡 Motivation
|
||||
|
||||
@ -71,20 +71,13 @@ Add Content Modal:
|
||||
|
||||
If you value this project, please think about awarding it a ⭐. Thanks ! 🙏
|
||||
|
||||
## 🗺️ Short-term roadmap
|
||||
- New `Composition` content type: Check out a [video demo here](https://demo.obscreen.io/data/uploads/compositions.mp4)
|
||||
- New `Text` Content Type: Display text with customizable styles, including options for scrolling effects.
|
||||
- New `HTML` Content Type: Display HTML snippets for more powerful text customization, giving you full control over the content.
|
||||
- Fleet Studio Management: Reviving a legacy feature
|
||||
- Remote Player Server: A new way to manage a player from the studio without needing SSH access to player
|
||||
|
||||
## 🛟 Discussion / Need help ?
|
||||
|
||||
### Join our Discord
|
||||
[<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/img/discord.png" width="64">](https://discord.obscreen.io)
|
||||
[<img src="https://github.com/jr-k/obscreen/blob/master/docs/img/discord.png" width="64">](https://discord.obscreen.io)
|
||||
|
||||
### Open an Issue
|
||||
[<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/img/github.png" width="64">](https://git.sumisu.xyz/csmith1865/obscreen/issues/new/choose)
|
||||
[<img src="https://github.com/jr-k/obscreen/blob/master/docs/img/github.png" width="64">](https://github.com/jr-k/obscreen/issues/new/choose)
|
||||
|
||||
### Troubleshoot
|
||||
|
||||
@ -94,7 +87,7 @@ If you value this project, please think about awarding it a ⭐. Thanks ! 🙏
|
||||
|
||||
This is "normal" behavior. Videos do not play automatically in Chrome because it requires user interaction with the page (a simple click inside the webpage is enough). If you open the console, you'll see the error: [Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first...](https://goo.gl/xX8pDD)
|
||||
|
||||
To resolve this, you need to use the Chrome flag `--autoplay-policy=no-user-gesture-required`. When connecting a Raspberry Pi with Obscreen Player autorun, this issue doesn't occur because the flag is handled automatically for you. You need to enable this flag yourself otherwise.
|
||||
To resolve this, you need to use the Chrome flag --autoplay-policy=no-user-gesture-required. When connecting a Raspberry Pi with Obscreen Player autorun, this issue doesn't occur because the flag is handled automatically for you.You need to enable this flag yourself otherwise.
|
||||
|
||||
---
|
||||
|
||||
@ -112,7 +105,7 @@ Check out the latest beta release here: https://github.com/jr-k/obscreen/release
|
||||
|
||||
### Translations
|
||||
|
||||
If you want to translate Obscreen into your language, please visit [Languages Files](https://git.sumisu.xyz/csmith1865/obscreen/src/branch/master/lang).
|
||||
If you want to translate Obscreen into your language, please visit [Languages Files](https://github.com/jr-k/obscreen/blob/master/lang).
|
||||
|
||||
### Spelling & Grammar
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -31,20 +31,13 @@ jQuery(function ($) {
|
||||
},
|
||||
always: function (e, data) {
|
||||
const response = data._response.jqXHR;
|
||||
let statusCode = response.status;
|
||||
$button.removeClass('uploading').removeClass('btn-naked btn-super-upload-busy').addClass('btn-info btn-super-upload');
|
||||
|
||||
let errorComment = response.responseText.match(/<!--\s*error=(\d+);\s*-->/);
|
||||
if (errorComment && errorComment[1]) {
|
||||
statusCode = parseInt(errorComment[1], 10);
|
||||
}
|
||||
|
||||
if (statusCode !== 200) {
|
||||
const $alert = $('.alert-upload').removeClass('hidden');
|
||||
if (statusCode === 413) {
|
||||
$alert.html(`<i class="fa fa-warning"></i>${l.js_common_http_error_413}`);
|
||||
if (response.status != 200) {
|
||||
const $alert = $('.alert-danger').removeClass('hidden');
|
||||
if (response.status == 413) {
|
||||
$alert.text(l.js_common_http_error_413);
|
||||
} else {
|
||||
$alert.html(`<i class="fa fa-warning"></i>${l.js_common_http_error_occured.replace('%code%', statusCode)}`);
|
||||
$alert.text(l.js_common_http_error_occured.replace('%code%', response.status));
|
||||
}
|
||||
} else {
|
||||
document.location.reload();
|
||||
@ -30,10 +30,6 @@ const hideDropdowns = function () {
|
||||
$('.dropdown').removeClass('dropdown-show');
|
||||
};
|
||||
|
||||
const classColorXor = function(color, fallback) {
|
||||
return color === 'gscaleF' ? 'gscale0' : (color === 'gscale0' ? 'gscaleF' : fallback);
|
||||
};
|
||||
|
||||
const showToast = function (text) {
|
||||
const $toast = $(".toast");
|
||||
$toast.addClass('show');
|
||||
@ -166,12 +162,5 @@ jQuery(document).ready(function ($) {
|
||||
|
||||
showToast(l.js_common_copied);
|
||||
});
|
||||
|
||||
$(window).on('beforeunload', function(event) {
|
||||
$('.modal').each(function() {
|
||||
$(this).find('button[type=submit]').removeClass('hidden');
|
||||
$(this).find('.btn-loading').addClass('hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
84
data/www/js/lib/jquery-more.js
vendored
84
data/www/js/lib/jquery-more.js
vendored
@ -1,84 +0,0 @@
|
||||
jQuery(function () {
|
||||
$(document).ready(function () {
|
||||
function adjustValue(inputElement, delta) {
|
||||
const currentValue = parseInt(inputElement.value) || 0;
|
||||
const newValue = currentValue + delta;
|
||||
if (("" + newValue).length <= inputElement.maxLength) {
|
||||
inputElement.value = newValue >= 0 ? newValue : 0;
|
||||
$(inputElement).trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
$('.numeric-input').on('input', function () {
|
||||
this.value = this.value.replace(/[^0-9]/g, '');
|
||||
});
|
||||
|
||||
$('.numeric-input').on('keydown', function (e) {
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
adjustValue(this, e.shiftKey ? 10 : 1);
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
adjustValue(this, e.shiftKey ? -10 : -1);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function updateRadioActiveClass() {
|
||||
$('.radio-group label').removeClass('active');
|
||||
$('input[type="radio"]:checked').next('label').addClass('active');
|
||||
}
|
||||
updateRadioActiveClass();
|
||||
$('.radio-group input[type="radio"]').change(function() {
|
||||
updateRadioActiveClass();
|
||||
});
|
||||
|
||||
|
||||
function updateCheckboxActiveClass() {
|
||||
$('.checkbox-group label').each(function() {
|
||||
const checkbox = $(this).prev('input[type="checkbox"]');
|
||||
if (checkbox.is(':checked')) {
|
||||
$(this).addClass('active');
|
||||
} else {
|
||||
$(this).removeClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
updateCheckboxActiveClass();
|
||||
$('.checkbox-group input[type="checkbox"]').change(function() {
|
||||
updateCheckboxActiveClass();
|
||||
});
|
||||
|
||||
$.fn.serializeObject = function() {
|
||||
const obj = {};
|
||||
|
||||
this.find('input, select, textarea').each(function() {
|
||||
const field = $(this);
|
||||
const name = field.attr('name');
|
||||
|
||||
if (!name) return; // Ignore fields without a name
|
||||
|
||||
if (field.is(':checkbox')) {
|
||||
const isOnOff = field.val() === 'on' || field.val() === '1';
|
||||
obj[name] = field.is(':checked') ? field.val() : (isOnOff ? false : null);
|
||||
} else if (field.is(':radio')) {
|
||||
if (field.is(':checked')) {
|
||||
obj[name] = field.val();
|
||||
} else if (!(name in obj)) {
|
||||
obj[name] = false;
|
||||
}
|
||||
} else {
|
||||
const tryInt = parseInt(field.val());
|
||||
obj[name] = isNaN(tryInt) ? field.val() : tryInt;
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
1
data/www/js/lib/jquery-ui-rotatable.min.js
vendored
1
data/www/js/lib/jquery-ui-rotatable.min.js
vendored
File diff suppressed because one or more lines are too long
1
data/www/js/lib/jscolor.min.js
vendored
1
data/www/js/lib/jscolor.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,471 +0,0 @@
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
const DEFAULT_RATIO = "16/9";
|
||||
const contentData = JSON.parse($('#content-edit-location').val() || `{"ratio":"${DEFAULT_RATIO}", "layers":{}}`);
|
||||
let currentElement = null;
|
||||
let elementCounter = 0;
|
||||
let screenRatio = 16/9;
|
||||
|
||||
const setRatio = function () {
|
||||
const ratioString = $('#elem-screen-ratio').val() || DEFAULT_RATIO;
|
||||
$('.ratio-value').text(ratioString.replace('/', ' / '));
|
||||
screenRatio = evalStringRatio(ratioString);
|
||||
$('.screen-holder').css({ 'padding-top': ( 1/ ( screenRatio ) * 100) + '%' });
|
||||
$('.ratio-value').val(screenRatio);
|
||||
$('#screen').css({
|
||||
width: $('#screen').width(),
|
||||
height: $('#screen').width() * (1/screenRatio),
|
||||
position: 'relative',
|
||||
}).parents('.screen-holder:eq(0)').css({
|
||||
width: 'auto',
|
||||
'padding-top': '0px'
|
||||
});
|
||||
};
|
||||
setRatio();
|
||||
|
||||
$(document).on('input', '#elem-screen-ratio', function() {
|
||||
setRatio();
|
||||
});
|
||||
|
||||
function createElement(config = null) {
|
||||
const screen = $('#screen');
|
||||
const screenWidth = screen.width();
|
||||
const screenHeight = screen.height();
|
||||
|
||||
const elementWidth = config ? (config.widthPercent / 100) * screenWidth : 100;
|
||||
const elementHeight = config ? (config.heightPercent / 100) * screenHeight : 50;
|
||||
let x = config ? (config.xPercent / 100) * screenWidth : Math.round(Math.random() * (screenWidth - elementWidth));
|
||||
let y = config ? (config.yPercent / 100) * screenHeight : Math.round(Math.random() * (screenHeight - elementHeight));
|
||||
const zIndex = config ? config.zIndex : elementCounter++;
|
||||
|
||||
//x = Math.round(Math.max(0, Math.min(x, screenWidth - elementWidth)));
|
||||
//y = Math.round(Math.max(0, Math.min(y, screenHeight - elementHeight)));
|
||||
|
||||
const elementId = zIndex;
|
||||
const element = $('<div class="element" id="element-' + zIndex + '" data-id="' + zIndex + '"><i class="fa fa-cog"></i></div>');
|
||||
// const element = $('<div class="element" id="' + elementId + '"><button>Button</button><div class="rotate-handle"></div></div>');
|
||||
|
||||
element.css({
|
||||
left: x,
|
||||
top: y,
|
||||
width: elementWidth,
|
||||
height: elementHeight,
|
||||
zIndex: zIndex,
|
||||
transform: `rotate(0deg)`
|
||||
});
|
||||
|
||||
element.draggable({
|
||||
// containment: "#screen",
|
||||
start: function (event, ui) {
|
||||
focusElement(ui.helper);
|
||||
},
|
||||
drag: function (event, ui) {
|
||||
updateForm(ui.helper);
|
||||
}
|
||||
});
|
||||
|
||||
element.resizable({
|
||||
// containment: "#screen",
|
||||
handles: 'n, s, e, w, nw, ne, sw, se',
|
||||
start: function (event, ui) {
|
||||
focusElement(ui.element);
|
||||
},
|
||||
resize: function (event, ui) {
|
||||
updateForm(ui.element);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
element.rotatable({
|
||||
handle: element.find('.rotate-handle'),
|
||||
rotate: function(event, ui) {
|
||||
updateForm(ui.element);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
element.click(function () {
|
||||
focusElement($(this));
|
||||
});
|
||||
|
||||
screen.append(element);
|
||||
addElementToList(elementId);
|
||||
|
||||
if (config !== null && config.contentId !== null) {
|
||||
element.attr('data-content-id', config.contentId);
|
||||
element.attr('data-content-name', config.contentName);
|
||||
element.attr('data-content-type', config.contentType);
|
||||
element.attr('data-content-metadata', config.contentMetadata);
|
||||
|
||||
applyContentToElement({
|
||||
id: config.contentId,
|
||||
name: config.contentName,
|
||||
type: config.contentType,
|
||||
metadata: config.contentMetadata,
|
||||
}, element);
|
||||
|
||||
updateForm(element);
|
||||
unfocusElements();
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
focusElement(element);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
$(document).on('click', '.element-adjust-aspect-ratio', function(){
|
||||
const metadata = currentElement.data('content-metadata');
|
||||
const ratio = metadata.height / metadata.width;
|
||||
$('#elem-height').val($('#elem-width').val() * ratio).trigger('input');
|
||||
$('#elem-width').val($('#elem-width').val()).trigger('input');
|
||||
});
|
||||
|
||||
$(document).on('click', '.element-list-item', function(){
|
||||
focusElement($('#element-' + $(this).attr('data-id')));
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove-element', function(){
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
removeElementById($(this).attr('data-id'));
|
||||
}
|
||||
});
|
||||
|
||||
function removeElementById(elementId) {
|
||||
$('.element[data-id='+elementId+'], .element-list-item[data-id='+elementId+']').remove();
|
||||
updateZIndexes();
|
||||
}
|
||||
|
||||
function addElementToList(elementId) {
|
||||
const listItem = `<div class="element-list-item" data-id="__ID__">
|
||||
<i class="fa fa-cog"></i>
|
||||
<div class="inner">
|
||||
<label>__EMPTY__ __ID__ </label>
|
||||
<button type="button" class="btn btn-naked remove-element" data-id="__ID__">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-neutral configure-element content-explr-picker" data-id="__ID__">
|
||||
<i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
$('#elementList').append(
|
||||
$(listItem
|
||||
.replace(/__ID__/g, elementId)
|
||||
.replace(/__EMPTY__/g, l.js_common_empty)
|
||||
)
|
||||
);
|
||||
updateZIndexes();
|
||||
}
|
||||
|
||||
function unfocusElements() {
|
||||
$('.element, .element-list-item').removeClass('focused');
|
||||
currentElement = null;
|
||||
updateForm(null);
|
||||
}
|
||||
|
||||
function focusElement($element) {
|
||||
unfocusElements();
|
||||
currentElement = $element;
|
||||
$element.addClass('focused');
|
||||
const listElement = $('.element-list-item[data-id="' + $element.attr('data-id') + '"]');
|
||||
listElement.addClass('focused');
|
||||
updateForm($element);
|
||||
|
||||
const contentType = $element.attr('data-content-type');
|
||||
$('.element-tool').addClass('hidden');
|
||||
|
||||
if (contentType) {
|
||||
if (contentType === 'picture' || contentType === 'video') {
|
||||
const contentMetadata = $element.data('content-metadata');
|
||||
if (contentMetadata.width && contentMetadata.height) {
|
||||
$('.element-tool.element-adjust-aspect-ratio-container').removeClass('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateForm($element) {
|
||||
if (!$element) {
|
||||
$('form#elementForm input').val('').prop('disabled', true);
|
||||
$('.form-element-properties').addClass('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$('.form-element-properties').removeClass('hidden');
|
||||
$('form#elementForm input').prop('disabled', false);
|
||||
|
||||
const offset = $element.position();
|
||||
|
||||
if (offset !== undefined) {
|
||||
$('#elem-x').val(offset.left);
|
||||
$('#elem-y').val(offset.top);
|
||||
$('#elem-width').val($element.width());
|
||||
$('#elem-height').val($element.height());
|
||||
}
|
||||
|
||||
$element.find('i').css('font-size', Math.min($element.width(), $element.height()) / 3);
|
||||
|
||||
/*
|
||||
const rotation = $element.css('transform');
|
||||
const values = rotation.split('(')[1].split(')')[0].split(',');
|
||||
const angle = Math.round(Math.atan2(values[1], values[0]) * (180/Math.PI));
|
||||
$('#elem-rotate').val(angle);
|
||||
*/
|
||||
}
|
||||
|
||||
$(document).on('input', '#elementForm input', function () {
|
||||
if (!currentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let x = Math.round(parseInt($('#elem-x').val()));
|
||||
let y = Math.round(parseInt($('#elem-y').val()));
|
||||
let width = Math.round(parseInt($('#elem-width').val()));
|
||||
let height = Math.round(parseInt($('#elem-height').val()));
|
||||
// let rotation = parseInt($('#elem-rotate').val());
|
||||
|
||||
// Constrain x and y
|
||||
// x = Math.max(0, Math.min(x, screenWidth - width));
|
||||
// y = Math.max(0, Math.min(y, screenHeight - height));
|
||||
|
||||
// Constrain width and height
|
||||
width = Math.min(width, screenWidth - x);
|
||||
height = Math.min(height, screenHeight - y);
|
||||
|
||||
currentElement.css({
|
||||
left: x,
|
||||
top: y,
|
||||
width: width,
|
||||
height: height
|
||||
// transform: `rotate(${rotation}deg)`
|
||||
});
|
||||
|
||||
// Update form values to reflect clamped values
|
||||
$('#elem-x').val(x);
|
||||
$('#elem-y').val(y);
|
||||
$('#elem-width').val(width);
|
||||
$('#elem-height').val(height);
|
||||
});
|
||||
|
||||
// $(document).on('click', '#addElement', function () {
|
||||
// createElement();
|
||||
// });
|
||||
|
||||
$(document).on('click', '#removeAllElements', function () {
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
$('.element, .element-list-item').remove();
|
||||
updateZIndexes();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('dblclick', '.element', function (e) {
|
||||
$('.content-explr-picker[data-id='+$(this).attr('data-id')+']').click();
|
||||
});
|
||||
|
||||
$(document).on('mousedown', function (e) {
|
||||
const keepFocusedElement = $(e.target).hasClass('element')
|
||||
|| $(e.target).hasClass('element-list-item')
|
||||
|| $(e.target).parents('.element:eq(0)').length !== 0
|
||||
|| $(e.target).parents('.element-list-item:eq(0)').length !== 0
|
||||
|| $(e.target).is('input,select,textarea')
|
||||
|| $(e.target).is('.page-panel.right-panel button,a,.btn')
|
||||
|
||||
if (!keepFocusedElement) {
|
||||
unfocusElements();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '#presetGrid2x2', function () {
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let elements = $('.element');
|
||||
if (elements.length < 4) {
|
||||
while (elements.length < 4) {
|
||||
createElement();
|
||||
elements = $('.element');
|
||||
}
|
||||
}
|
||||
|
||||
elements = $('.element-list-item').map(function() {
|
||||
return $('.element[data-id='+$(this).attr('data-id')+']');
|
||||
}).slice(0, 4);
|
||||
|
||||
const gridPositions = [
|
||||
{x: 0, y: 0},
|
||||
{x: screenWidth / 2, y: 0},
|
||||
{x: 0, y: screenHeight / 2},
|
||||
{x: screenWidth / 2, y: screenHeight / 2}
|
||||
];
|
||||
|
||||
elements.each(function (index) {
|
||||
const position = gridPositions[index];
|
||||
$(this).css({
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
width: screenWidth / 2,
|
||||
height: screenHeight / 2
|
||||
});
|
||||
updateForm($(this));
|
||||
});
|
||||
|
||||
unfocusElements();
|
||||
});
|
||||
|
||||
$(document).on('click', '#presetTvNews1x1', function () {
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let elements = $('.element');
|
||||
if (elements.length === 0) {
|
||||
createElement();
|
||||
}
|
||||
|
||||
if (!currentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const height = (screenHeight / 7);
|
||||
currentElement.css({
|
||||
left: 0,
|
||||
top: screenHeight - height,
|
||||
width: screenWidth,
|
||||
height: height
|
||||
});
|
||||
updateForm(currentElement);
|
||||
unfocusElements();
|
||||
});
|
||||
|
||||
$(document).keydown(function (e) {
|
||||
if (e.key === "Escape") {
|
||||
unfocusElements();
|
||||
}
|
||||
|
||||
const hasFocusInInput = $('input,textarea').is(':focus');
|
||||
|
||||
if (!currentElement || hasFocusInInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowLeft") {
|
||||
$('#elem-x').val(parseInt($('#elem-x').val()) - (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowRight") {
|
||||
$('#elem-x').val(parseInt($('#elem-x').val()) + (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowUp") {
|
||||
$('#elem-y').val(parseInt($('#elem-y').val()) - (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowDown") {
|
||||
$('#elem-y').val(parseInt($('#elem-y').val()) + (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "Backspace") {
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
removeElementById(currentElement.attr('data-id'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.content-explr-picker', function () {
|
||||
const elementId = $(this).attr('data-id');
|
||||
const isNew = !elementId;
|
||||
const $element = isNew ? $(createElement()) : $('#element-'+elementId);
|
||||
|
||||
showPickers('modal-content-explr-picker', function (content) {
|
||||
applyContentToElement(content, $element)
|
||||
});
|
||||
});
|
||||
|
||||
const applyContentToElement = function (content, $element) {
|
||||
$element.attr('data-content-id', content.id);
|
||||
$element.attr('data-content-name', content.name);
|
||||
$element.attr('data-content-type', content.type);
|
||||
$element.data('content-metadata', content.metadata);
|
||||
const $elementList = $('.element-list-item[data-id='+$element.attr('data-id')+']');
|
||||
const iconClasses = [
|
||||
'fa',
|
||||
content_type_icon_classes[content.type],
|
||||
content_type_color_classes[content.type]
|
||||
].join(' ');
|
||||
$element.find('i').get(0).classList = iconClasses;
|
||||
$elementList.find('label').text(content.name);
|
||||
$elementList.find('i:eq(0)').get(0).classList = iconClasses;
|
||||
};
|
||||
|
||||
$(document).on('submit', 'form.form', function (e) {
|
||||
unfocusElements();
|
||||
const location = getLocationPayload();
|
||||
$('#content-edit-location').val(JSON.stringify(location));
|
||||
});
|
||||
|
||||
function updateZIndexes() {
|
||||
const zindex = $('.element-list-item').length + 1;
|
||||
$('.element-list-item').each(function(index) {
|
||||
const id = $(this).attr('data-id');
|
||||
$('#element-' + id).css('z-index', zindex - index);
|
||||
});
|
||||
}
|
||||
|
||||
$('#elementList').sortable({
|
||||
update: function(event, ui) {
|
||||
updateZIndexes();
|
||||
}
|
||||
});
|
||||
|
||||
const applyElementsFromContent = function() {
|
||||
for (let i = 0; i < contentData.layers.length; i++) {
|
||||
createElement(contentData.layers[i]);
|
||||
}
|
||||
};
|
||||
|
||||
applyElementsFromContent();
|
||||
|
||||
const getLocationPayload = function() {
|
||||
const screen = $('#screen');
|
||||
const screenWidth = screen.width();
|
||||
const screenHeight = screen.height();
|
||||
const layers = [];
|
||||
|
||||
$('.element').each(function () {
|
||||
const $element = $(this);
|
||||
const offset = $element.position();
|
||||
const x = offset.left;
|
||||
const y = offset.top;
|
||||
const width = $element.width();
|
||||
const height = $element.height();
|
||||
|
||||
const xPercent = (x / screenWidth) * 100;
|
||||
const yPercent = (y / screenHeight) * 100;
|
||||
const widthPercent = (width / screenWidth) * 100;
|
||||
const heightPercent = (height / screenHeight) * 100;
|
||||
const contentId = $element.attr('data-content-id');
|
||||
const contentName = $element.attr('data-content-name');
|
||||
const contentType = $element.attr('data-content-type');
|
||||
const contentMetadata = $element.data('content-metadata');
|
||||
|
||||
const layer = {
|
||||
xPercent: xPercent,
|
||||
yPercent: yPercent,
|
||||
widthPercent: widthPercent,
|
||||
heightPercent: heightPercent,
|
||||
zIndex: parseInt($element.css('zIndex')),
|
||||
contentId: contentId ? parseInt(contentId) : null,
|
||||
contentName: contentName ? contentName : null,
|
||||
contentType: contentType ? contentType : null,
|
||||
contentMetadata: contentMetadata && contentMetadata !== "null" ? contentMetadata : null,
|
||||
};
|
||||
|
||||
layers.push(layer);
|
||||
});
|
||||
|
||||
layers.sort(function(a, b) {
|
||||
return parseInt(b.zIndex) - parseInt(a.zIndex);
|
||||
});
|
||||
|
||||
return {
|
||||
ratio: $('#elem-screen-ratio').val(),
|
||||
layers: layers
|
||||
};
|
||||
};
|
||||
});
|
||||
@ -1,79 +0,0 @@
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
const contentData = JSON.parse($('#content-edit-location').val() || '{}');
|
||||
const screenRatio = 16/9;
|
||||
|
||||
$('.screen-holder').css({
|
||||
'padding-top': ( 1/ ( screenRatio ) * 100) + '%'
|
||||
});
|
||||
|
||||
$('.ratio-value').val(screenRatio);
|
||||
|
||||
$('#screen').css({
|
||||
width: $('#screen').width(),
|
||||
height: $('#screen').height(),
|
||||
position: 'relative',
|
||||
}).parents('.screen-holder:eq(0)').css({
|
||||
width: 'auto',
|
||||
'padding-top': '0px'
|
||||
});
|
||||
|
||||
const draw = function() {
|
||||
const $screen = $('#screen');
|
||||
const $text = $('<div class="text">');
|
||||
let insideText = $('#elem-text').val();
|
||||
|
||||
if ($('#elem-scroll-enable').is(':checked')) {
|
||||
const $wrapper = $('<marquee>');
|
||||
$wrapper.attr({
|
||||
scrollamount: $('#elem-scroll-speed').val(),
|
||||
direction: $('[name=scrollDirection]:checked').val(),
|
||||
behavior: 'scroll',
|
||||
loop: -1
|
||||
});
|
||||
$wrapper.append(insideText);
|
||||
insideText = $wrapper;
|
||||
}
|
||||
|
||||
$text.append(insideText);
|
||||
|
||||
let justifyContent = 'center';
|
||||
switch($('[name=textAlign]:checked').val()) {
|
||||
case 'left': justifyContent = 'flex-start'; break;
|
||||
case 'right': justifyContent = 'flex-end'; break;
|
||||
}
|
||||
|
||||
$text.css({
|
||||
padding: $('#elem-container-margin').val() + 'px',
|
||||
color: $('#elem-fg-color').val(),
|
||||
textAlign: $('[name=textAlign]:checked').val(),
|
||||
textDecoration: $('#elem-text-underline').is(':checked') ? 'underline' : 'normal',
|
||||
fontSize: $('#elem-font-size').val() + 'px',
|
||||
fontWeight: $('#elem-font-bold').is(':checked') ? 'bold' : 'normal',
|
||||
fontStyle: $('#elem-font-italic').is(':checked') ? 'italic' : 'normal',
|
||||
fontFamily: $('#elem-font-family').val() + ", 'Arial', 'sans-serif'",
|
||||
whiteSpace: $('#elem-single-line').is(':checked') ? 'nowrap' : 'normal',
|
||||
justifyContent: justifyContent
|
||||
});
|
||||
|
||||
$screen.css({
|
||||
backgroundColor: $('#elem-bg-color').val(),
|
||||
});
|
||||
|
||||
$screen.html($text);
|
||||
};
|
||||
|
||||
$(document).on('input', '#elementForm input, #elementForm select', function () {
|
||||
draw();
|
||||
});
|
||||
|
||||
draw();
|
||||
|
||||
|
||||
$(document).on('submit', 'form.form', function (e) {
|
||||
const location = $('form#elementForm').serializeObject();
|
||||
$('#content-edit-location').val(JSON.stringify(location));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -21,11 +21,7 @@ jQuery(document).ready(function ($) {
|
||||
$form.find('.object-label:visible').html(optionAttributes['data-object-label'].value);
|
||||
$('.type-icon').attr('class', 'type-icon fa ' + optionAttributes['data-icon'].value);
|
||||
$('.tab-select .widget').attr('class', 'widget ' + ('border-' + color) + ' ' + color);
|
||||
$form.find('button[type=submit]').attr('class', [
|
||||
'btn',
|
||||
`btn-${color}`,
|
||||
classColorXor(color, '')
|
||||
].join(' '));
|
||||
$form.find('button[type=submit]').attr('class', 'btn ' + ('btn-' + color));
|
||||
};
|
||||
|
||||
const main = function () {
|
||||
|
||||
@ -81,9 +81,3 @@ const secondsToHHMMSS = function (seconds) {
|
||||
const secs = seconds % 60;
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const evalStringRatio = function(str) {
|
||||
return str.replace(/(\d+)\/(\d+)/g, function(match, p1, p2) {
|
||||
return (parseInt(p1) / parseInt(p2)).toString();
|
||||
});
|
||||
};
|
||||
|
||||
@ -31,16 +31,8 @@ main {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.context-tail {
|
||||
margin-right: 30px;
|
||||
|
||||
.btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.context-tail-auth {
|
||||
margin-right: 10px;
|
||||
.contex-tail {
|
||||
margin-right: 20px;
|
||||
|
||||
.btn {
|
||||
margin-right: 0;
|
||||
|
||||
@ -26,10 +26,6 @@ body, html {
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
&.fx-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical {
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
@keyframes blink{50%{opacity:0;}}
|
||||
.cfx-blink{animation:1.5s linear infinite blink;}
|
||||
.cfx-ffff-speed {animation-delay: 0.1s;}
|
||||
.cfx-fff-speed {animation-delay: 0.3s;}
|
||||
.cfx-ff-speed {animation-delay: 0.5s;}
|
||||
.cfx-f-speed {animation-delay: 0.8s;}
|
||||
.cfx-m-speed {animation-delay: 1s;}
|
||||
.cfx-s-speed {animation-delay: 1.3s;}
|
||||
.cfx-ss-speed {animation-delay: 1.5s;}
|
||||
.cfx-sss-speed {animation-delay: 1.8s;}
|
||||
.cfx-ssss-speed {animation-delay: 2s;}
|
||||
.cfx-sssss-speed {animation-delay: 3s;}
|
||||
@ -1,10 +1,32 @@
|
||||
.badge-inset {
|
||||
display: inline;
|
||||
color: $gscaleA;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
background: $gscale0;
|
||||
border: 1px solid $gscale3;
|
||||
border-radius: $baseRadius;
|
||||
padding: 3px 7px;
|
||||
a.badge,
|
||||
.badge {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px 5px;
|
||||
border-radius: $baseRadius;
|
||||
font-size: 12px;
|
||||
background: rgba($gscaleF, .1);
|
||||
border: 1px solid transparent;
|
||||
color: $gscaleF;
|
||||
}
|
||||
|
||||
a.badge:hover {
|
||||
color: $gscaleF;
|
||||
border: 1px solid rgba($gscaleF, .4);
|
||||
}
|
||||
|
||||
.panel-inactive .badge {
|
||||
background: rgba($gscale7, .1);
|
||||
color: $gscale7;
|
||||
}
|
||||
|
||||
.panel-inactive a.badge:hover {
|
||||
color: $gscale7;
|
||||
border: 1px solid rgba($gscale7,.2);
|
||||
}
|
||||
|
||||
.badge.anonymous {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
button,
|
||||
.btn {
|
||||
$shadowOffset: 2px;
|
||||
@ -57,7 +56,6 @@ button,
|
||||
box-shadow: 0 $shadowOffset 0 0 darken($gscale5, 10%);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
box-shadow: 0 $shadowOffset 0 1px $gkscale2 inset;
|
||||
background: darken($gscale5, 10%);
|
||||
@ -143,3 +141,4 @@ button,
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -81,24 +81,6 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group,
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0 5px 0 0 !important;
|
||||
justify-content: center !important;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.widget {
|
||||
margin-top: 10px;
|
||||
align-self: stretch;
|
||||
@ -118,11 +100,12 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
input + .btn + .btn {
|
||||
.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&.widget-unit {
|
||||
|
||||
select,
|
||||
input {
|
||||
flex-grow: 0;
|
||||
@ -148,33 +131,6 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
input {
|
||||
&.size-m {
|
||||
max-width: 122px;
|
||||
}
|
||||
|
||||
&.color-picker {
|
||||
max-width: 125px;
|
||||
}
|
||||
|
||||
&.chars-4 {
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
&.chars-3 {
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
&.chars-2 {
|
||||
max-width: 20px;
|
||||
}
|
||||
|
||||
&.chars-1 {
|
||||
max-width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
color: rgba($gscaleF, .7);
|
||||
font-size: 14px;
|
||||
@ -199,17 +155,23 @@ form {
|
||||
color: $gscale5;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid $gscale3;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&.input-naked {
|
||||
padding-left: 0;
|
||||
color: $gscaleB;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&[disabled] {
|
||||
border: none;
|
||||
background: $gscale0;
|
||||
border-radius: $baseRadius;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,20 +18,19 @@
|
||||
@import 'components/modals';
|
||||
@import 'components/toast';
|
||||
@import 'components/dragdrop';
|
||||
@import 'components/animation';
|
||||
|
||||
// Legacy
|
||||
@import 'components/panes';
|
||||
@import 'components/tiles';
|
||||
@import 'components/empty';
|
||||
@import 'components/switches';
|
||||
@import 'components/badges';
|
||||
//@import 'components/badges';
|
||||
|
||||
// Import form styles
|
||||
@import 'forms/forms';
|
||||
|
||||
// Import pages styles
|
||||
@import 'pages/content';
|
||||
@import 'pages/content-composition';
|
||||
@import 'pages/content-text';
|
||||
@import 'pages/logs';
|
||||
@import 'pages/node-player';
|
||||
@import 'pages/playlist';
|
||||
|
||||
@ -43,25 +43,6 @@ button,
|
||||
&.btn-neutral:hover {
|
||||
box-shadow: 0 2px 0 1px $gkscale6 inset;
|
||||
}
|
||||
|
||||
&.btn-neutral {
|
||||
$shadowOffset: 2;
|
||||
color: $gkscale5;
|
||||
background: $white;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
box-shadow: 0 $shadowOffset 0 1px $gkscale2 inset;
|
||||
background: $gkscaleC;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: darken($gscale5, 20%);
|
||||
border: 1px solid $gscaleA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiles .tiles-inner .tile-item {
|
||||
|
||||
@ -1,364 +0,0 @@
|
||||
.view-content-edit.view-content-edit-composition main .main-container {
|
||||
|
||||
.page-panel.left-panel {
|
||||
flex: 1;
|
||||
|
||||
.form-holder {
|
||||
margin: 20px 20px 20px 10px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.page-panel.right-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h3.main {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin-top: 5px;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h4 {
|
||||
margin-right: 5px;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button {
|
||||
padding: 3px 15px;
|
||||
margin:0 3px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
min-height: initial;
|
||||
border: 1px solid $gkscale3;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-holder {
|
||||
//display: flex;
|
||||
//flex-direction: row;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: 56.25%; /* 16:9 aspect ratio */
|
||||
overflow: hidden;
|
||||
border-radius: $baseRadius;
|
||||
outline: 4px solid rgba($gscaleF, .1);
|
||||
|
||||
.screen {
|
||||
background-color: #ddd;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
|
||||
.element {
|
||||
position: absolute !important;
|
||||
background-color: $gkscaleE;
|
||||
outline: 1px solid $gkscaleC;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.focused {
|
||||
border: none;
|
||||
outline: 2px solid $seaBlue;
|
||||
z-index: 89 !important;
|
||||
|
||||
.ui-resizable-handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: $gkscaleC;
|
||||
|
||||
&.fa-cog {
|
||||
text-shadow: 0 -2px $gkscaleB, 0 0px 2px $gkscaleB;
|
||||
}
|
||||
|
||||
&.gscaleF {
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: red;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -15px;
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.ui-resizable-handle {
|
||||
$size: 10px;
|
||||
$sizeOffset: -1*calc($size/2);
|
||||
background: $gkscaleA;
|
||||
border: 1px solid $gkscale5;
|
||||
width: $size;
|
||||
height: $size;
|
||||
z-index: 90;
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
&.ui-resizable-n {
|
||||
cursor: n-resize;
|
||||
top: $sizeOffset;
|
||||
left: 50%;
|
||||
margin-left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-s {
|
||||
cursor: s-resize;
|
||||
bottom: $sizeOffset;
|
||||
left: 50%;
|
||||
margin-left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-w {
|
||||
cursor: w-resize;
|
||||
left: $sizeOffset;
|
||||
top: 50%;
|
||||
margin-top: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-e {
|
||||
cursor: e-resize;
|
||||
right: $sizeOffset;
|
||||
top: 50%;
|
||||
margin-top: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-nw {
|
||||
cursor: nw-resize;
|
||||
top: $sizeOffset;
|
||||
left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-ne {
|
||||
cursor: ne-resize;
|
||||
top: $sizeOffset;
|
||||
right: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-sw {
|
||||
cursor: sw-resize;
|
||||
bottom: $sizeOffset;
|
||||
left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-se {
|
||||
cursor: se-resize;
|
||||
bottom: $sizeOffset;
|
||||
right: $sizeOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.elements-holder {
|
||||
align-self: stretch;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin: 0 0 20px 0;
|
||||
|
||||
&.divide {
|
||||
border-top: 1px solid $gscale2;
|
||||
margin-top: 10px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-elements-list {
|
||||
padding: 10px;
|
||||
background: $gscale2;
|
||||
border-radius: $baseRadius;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-self: flex-start;
|
||||
|
||||
.element-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
> i {
|
||||
color: $gscaleE;
|
||||
margin:0 10px 0 0;
|
||||
cursor: move;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inner:hover,
|
||||
&.focused .inner {
|
||||
background-color: $seaBlue;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
|
||||
button.btn-naked {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
cursor: pointer;
|
||||
padding: 5px 5px 5px 10px;
|
||||
margin-bottom: 5px;
|
||||
background: $gkscaleE;
|
||||
border-radius: $baseRadius;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
color: $gkscale2;
|
||||
min-height: 46px;
|
||||
flex: 1;
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 219px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
display: none;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
button.btn-naked {
|
||||
color: $gscale5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
label {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-element-properties {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.divide {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
|
||||
&[disabled] {
|
||||
padding: 8px 0 5px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, .05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,155 +0,0 @@
|
||||
.view-content-edit.view-content-edit-text main .main-container {
|
||||
|
||||
.page-panel.left-panel {
|
||||
flex: 1;
|
||||
|
||||
.form-holder {
|
||||
margin: 20px 20px 20px 10px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.page-panel.right-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h3.main {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin-top: 5px;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.screen-holder {
|
||||
//display: flex;
|
||||
//flex-direction: row;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: 56.25%; /* 16:9 aspect ratio */
|
||||
overflow: hidden;
|
||||
border-radius: $baseRadius;
|
||||
outline: 4px solid rgba($gscaleF, .1);
|
||||
background: repeating-conic-gradient(#EEE 0% 25%, white 0% 50%) 50% / 20px 20px;
|
||||
|
||||
.screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: flex;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
|
||||
marquee {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-element-properties {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.divide {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
label {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
|
||||
&[disabled] {
|
||||
padding: 8px 0 5px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, .05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -23,29 +23,6 @@
|
||||
|
||||
.view-content-edit main .main-container {
|
||||
|
||||
.top-content {
|
||||
h3 {
|
||||
color: $gscaleF;
|
||||
padding: 10px 10px 10px 0;
|
||||
font-size: 16px;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: $baseRadius;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-content {
|
||||
.page-content {
|
||||
flex: 1;
|
||||
@ -61,11 +38,32 @@
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
color: $gscaleF;
|
||||
padding: 10px 10px 10px 0;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
align-self: stretch;
|
||||
margin-left: -8px;
|
||||
|
||||
span {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: $baseRadius;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -89,3 +87,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ $layoutBorder: 1px solid $gscale2;
|
||||
// Packs
|
||||
$colors: (
|
||||
warning: $warning,
|
||||
orange: $orange,
|
||||
info: $info,
|
||||
info-alt: $bitterBlue,
|
||||
success: $success,
|
||||
@ -40,8 +39,6 @@ $colors: (
|
||||
redhat:$redhat,
|
||||
centos:$centos,
|
||||
other:$other,
|
||||
gscale0:$gscale0,
|
||||
gscaleF:$gscaleF,
|
||||
);
|
||||
|
||||
// Classes
|
||||
|
||||
@ -2,7 +2,7 @@ services:
|
||||
webapp:
|
||||
container_name: obscreen
|
||||
restart: unless-stopped
|
||||
image: csmith1865/obscreen:latest
|
||||
image: jierka/obscreen:latest
|
||||
environment:
|
||||
- DEMO=false
|
||||
- DEBUG=false
|
||||
|
||||
@ -62,20 +62,10 @@ docker compose up --detach --pull=always
|
||||
#### Install
|
||||
- Install studio by executing following script
|
||||
|
||||
##### Linux
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-studio.sh -o /tmp/install-studio.sh && chmod +x /tmp/install-studio.sh && sudo /bin/bash /tmp/install-studio.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
##### Windows & MacOS
|
||||
```bash
|
||||
git clone https://github.com/jr-k/obscreen.git
|
||||
cd obscreen
|
||||
python3 -m venv venv
|
||||
source ./venv/bin/activate
|
||||
pip install .
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
#### Configure
|
||||
- Server configuration is editable in `.env` file.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# <img src="https://raw.githubusercontent.com/csmith1865/obscreen/refs/heads/master/docs/img/obscreen.png" width="22"> Obscreen - Autorun on RaspberryPi
|
||||
# <img src="https://github.com/jr-k/obscreen/blob/master/docs/img/obscreen.png" width="22"> Obscreen - Autorun on RaspberryPi
|
||||
|
||||
> #### 👈 [back to readme](../README.md)
|
||||
> #### 👈 [back to readme](/README.md)
|
||||
|
||||
#### 🔴 You want to power RaspberryPi and automatically see your slideshow on a screen connected to it and manage your slideshow ? You're in the right place.
|
||||
|
||||
@ -20,20 +20,10 @@
|
||||
#### Install
|
||||
- Install studio by executing following script
|
||||
|
||||
##### Linux
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/csmith1865/obscreen/master/system/install-studio.sh -o /tmp/install-studio.sh && chmod +x /tmp/install-studio.sh && sudo /bin/bash /tmp/install-studio.sh $USER $HOME
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-studio.sh -o /tmp/install-studio.sh && chmod +x /tmp/install-studio.sh && sudo /bin/bash /tmp/install-studio.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
##### Windows & MacOS
|
||||
```bash
|
||||
git clone https://github.com/csmith1865/obscreen.git
|
||||
cd obscreen
|
||||
python3 -m venv venv
|
||||
source ./venv/bin/activate
|
||||
pip install .
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
#### Configure
|
||||
- Server configuration is editable in `.env` file.
|
||||
@ -82,7 +72,7 @@ docker run --restart=always --name obscreen --pull=always \
|
||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||
|
||||
# Download docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/csmith1865/obscreen/master/docker-compose.yml > docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/docker-compose.yml > docker-compose.yml
|
||||
|
||||
# Run
|
||||
docker compose up --detach --pull=always
|
||||
@ -106,7 +96,7 @@ docker compose up --detach --pull=always
|
||||
#### How to install
|
||||
- Install player autorun by executing following script (will install chromium, x11, pulseaudio and obscreen-player systemd service)
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/csmith1865/obscreen/master/system/install-player-rpi.sh -o /tmp/install-player-rpi.sh && chmod +x /tmp/install-player-rpi.sh && sudo /bin/bash /tmp/install-player-rpi.sh $USER $HOME
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-player-rpi.sh -o /tmp/install-player-rpi.sh && chmod +x /tmp/install-player-rpi.sh && sudo /bin/bash /tmp/install-player-rpi.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
|
||||
27
lang/en.json
27
lang/en.json
@ -59,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Are you sure?",
|
||||
"slideshow_content_page_title": "Content Library",
|
||||
"slideshow_content_button_add": "New Content",
|
||||
"slideshow_content_referenced_in_slide_error": "Content '%contentName%' is referenced in a slide, remove slide first",
|
||||
"slideshow_content_referenced_in_slide_error": "Content is referenced in a slide, remove slide first",
|
||||
"slideshow_content_panel_active": "Content",
|
||||
"slideshow_content_panel_empty": "Currently, there are no content. %link% now.",
|
||||
"slideshow_content_panel_th_name": "Name",
|
||||
@ -255,27 +255,12 @@
|
||||
"common_apply": "Apply",
|
||||
"common_saved": "Changes have been saved",
|
||||
"common_new_folder": "New Folder",
|
||||
"common_folder_not_empty_error": "Folder '%folderName%' isn't empty, you must delete its content first",
|
||||
"common_folder_not_empty_error": "Folder isn't empty, you must delete its content first",
|
||||
"common_copied": "Element copied in clipboard!",
|
||||
"common_host_placeholder": "raspberrypi.local or 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Error %code% occured",
|
||||
"common_http_error_413": "Files are too large",
|
||||
"common_width": "Width",
|
||||
"common_height": "Height",
|
||||
"common_position": "Position",
|
||||
"common_angle": "Angle",
|
||||
"common_size": "Dimensions",
|
||||
"composition_elements_heading": "Elements",
|
||||
"composition_element_add": "Add element",
|
||||
"composition_elements_delete_all": "Delete all",
|
||||
"composition_presets": "Presets",
|
||||
"composition_presets_grid_2x2": "Grid 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Screen",
|
||||
"composition_element_x_axis": "X axis",
|
||||
"composition_element_y_axis": "Y axis",
|
||||
"composition_element_match_content_aspect_ratio": "Match content aspect ratio",
|
||||
"logout": "Logout",
|
||||
"login_error_not_found": "Bad credentials",
|
||||
"login_error_bad_credentials": "Bad credentials",
|
||||
@ -307,10 +292,6 @@
|
||||
"enum_content_type_external_storage": "External Storage",
|
||||
"enum_content_type_external_storage_object_label": "Specify an existing directory relative to the following path",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Path relative to a removeable device",
|
||||
"enum_content_type_composition": "Composition",
|
||||
"enum_content_type_composition_object_label": "Screen aspect ratio",
|
||||
"enum_content_type_text": "Text",
|
||||
"enum_content_type_text_object_label": "Displayed text",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Picture",
|
||||
@ -329,8 +310,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Other",
|
||||
"sysinfo_device_model": "Device model",
|
||||
"sysinfo_device_model_unknown": "Unknown model",
|
||||
"sysinfo_rpi_model": "Raspberry Pi Model",
|
||||
"sysinfo_rpi_model_unknown": "Not a Raspberry Pi or model information not available",
|
||||
"sysinfo_storage_free_space": "Storage Free Space",
|
||||
"sysinfo_memory_usage": "Memory Usage",
|
||||
"sysinfo_os_version": "OS Version",
|
||||
|
||||
27
lang/es.json
27
lang/es.json
@ -59,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "¿Estás seguro?",
|
||||
"slideshow_content_page_title": "Biblioteca de contenidos",
|
||||
"slideshow_content_button_add": "Nuevo Contenido",
|
||||
"slideshow_content_referenced_in_slide_error": "Se hace referencia al contenido '%contentName%' en una diapositiva; elimine la diapositiva primero",
|
||||
"slideshow_content_referenced_in_slide_error": "Se hace referencia al contenido en una diapositiva; elimine la diapositiva primero",
|
||||
"slideshow_content_panel_active": "Contenido",
|
||||
"slideshow_content_panel_empty": "Actualmente, no hay contenido. %link% ahora.",
|
||||
"slideshow_content_panel_th_name": "Nombre",
|
||||
@ -256,27 +256,12 @@
|
||||
"common_apply": "Aplicar",
|
||||
"common_saved": "Los cambios se han guardado",
|
||||
"common_new_folder": "Nuevo Carpeta",
|
||||
"common_folder_not_empty_error": "La carpeta '%folderName%' no está vacía, primero debes eliminar su contenido",
|
||||
"common_folder_not_empty_error": "La carpeta no está vacía, primero debes eliminar su contenido",
|
||||
"common_copied": "¡Elemento copiado!",
|
||||
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Se ha producido un error %code%",
|
||||
"common_http_error_413": "Los archivos son demasiado grandes",
|
||||
"common_width": "Ancho",
|
||||
"common_height": "Altura",
|
||||
"common_position": "Posición",
|
||||
"common_angle": "Ángulo",
|
||||
"common_size": "Dimensiones",
|
||||
"composition_elements_heading": "Elementos",
|
||||
"composition_element_add": "Añadir elemento",
|
||||
"composition_elements_delete_all": "Eliminar todo",
|
||||
"composition_presets": "Preajustes",
|
||||
"composition_presets_grid_2x2": "Cuadrícula 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Pantalla",
|
||||
"composition_element_x_axis": "Eje X",
|
||||
"composition_element_y_axis": "Eje Y",
|
||||
"composition_element_match_content_aspect_ratio": "Ajustar la escala del contenido",
|
||||
"logout": "Cerrar sesión",
|
||||
"login_error_not_found": "Credenciales incorrectas",
|
||||
"login_error_bad_credentials": "Credenciales incorrectas",
|
||||
@ -308,10 +293,6 @@
|
||||
"enum_content_type_external_storage": "Almacenamiento externo",
|
||||
"enum_content_type_external_storage_object_label": "Especifique un directorio existente relativo a la siguiente ruta",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Ruta relativa a un dispositivo extraíble",
|
||||
"enum_content_type_composition": "Composición",
|
||||
"enum_content_type_composition_object_label": "Relación de aspecto de la pantalla",
|
||||
"enum_content_type_text": "Texto",
|
||||
"enum_content_type_text_object_label": "Texto mostrado",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Imagen",
|
||||
@ -330,8 +311,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Otro",
|
||||
"sysinfo_device_model": "Modelo del dispositivo",
|
||||
"sysinfo_device_model_unknown": "Modelo desconocido",
|
||||
"sysinfo_rpi_model": "Modelo de Raspberry Pi",
|
||||
"sysinfo_rpi_model_unknown": "No es una Raspberry Pi o la información del modelo no está disponible",
|
||||
"sysinfo_storage_free_space": "Espacio de almacenamiento libre",
|
||||
"sysinfo_memory_usage": "Uso de memoria",
|
||||
"sysinfo_os_version": "Versión del SO",
|
||||
|
||||
27
lang/fr.json
27
lang/fr.json
@ -59,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
|
||||
"slideshow_content_page_title": "Bibliothèque de contenus",
|
||||
"slideshow_content_button_add": "Nouveau Contenu",
|
||||
"slideshow_content_referenced_in_slide_error": "Le contenu '%contentName%' est référencé dans une slide, supprimez d'abord la slide",
|
||||
"slideshow_content_referenced_in_slide_error": "Le contenu est référencé dans une slide, supprimez d'abord la slide",
|
||||
"slideshow_content_panel_active": "Contenus",
|
||||
"slideshow_content_panel_empty": "Actuellement, il n'y a aucun contenu. %link% maintenant.",
|
||||
"slideshow_content_panel_th_name": "Nom",
|
||||
@ -257,27 +257,12 @@
|
||||
"common_apply": "Appliquer",
|
||||
"common_saved": "Les modifications ont été enregistrées",
|
||||
"common_new_folder": "Nouveau Dossier",
|
||||
"common_folder_not_empty_error": "Le dossier '%folderName%' n'est pas vide, vous devez d'abord supprimer son contenu",
|
||||
"common_folder_not_empty_error": "Le dossier n'est pas vide, vous devez d'abord supprimer son contenu",
|
||||
"common_copied": "Element copié !",
|
||||
"common_host_placeholder": "raspberrypi.local ou 192.168.1.85",
|
||||
"common_reachable_at": "Hôte",
|
||||
"common_http_error_occured": "Une erreur %code% est apparue",
|
||||
"common_http_error_413": "Les fichiers sont trop volumineux",
|
||||
"common_width": "Largeur",
|
||||
"common_height": "Hauteur",
|
||||
"common_position": "Position",
|
||||
"common_angle": "Angle",
|
||||
"common_size": "Dimensions",
|
||||
"composition_elements_heading": "Éléments",
|
||||
"composition_element_add": "Ajouter un élément",
|
||||
"composition_elements_delete_all": "Tout supprimer",
|
||||
"composition_presets": "Préréglages",
|
||||
"composition_presets_grid_2x2": "Grille 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Écran",
|
||||
"composition_element_x_axis": "Axe X",
|
||||
"composition_element_y_axis": "Axe Y",
|
||||
"composition_element_match_content_aspect_ratio": "Ajuster l'échelle du contenu",
|
||||
"logout": "Déconnexion",
|
||||
"login_error_not_found": "Identifiants invalides",
|
||||
"login_error_bad_credentials": "Identifiants invalides",
|
||||
@ -309,10 +294,6 @@
|
||||
"enum_content_type_external_storage": "Stockage externe",
|
||||
"enum_content_type_external_storage_object_label": "Spécifiez un répertoire existant par rapport au chemin suivant",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Chemin relatif à un périphérique amovible",
|
||||
"enum_content_type_composition": "Composition",
|
||||
"enum_content_type_composition_object_label": "Rapport hauteur/largeur de l'écran",
|
||||
"enum_content_type_text": "Texte",
|
||||
"enum_content_type_text_object_label": "Texte affiché",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Vidéo",
|
||||
"enum_content_type_picture": "Image",
|
||||
@ -331,8 +312,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Autre",
|
||||
"sysinfo_device_model": "Modèle de l'appareil",
|
||||
"sysinfo_device_model_unknown": "Modèle inconnu",
|
||||
"sysinfo_rpi_model": "Modèle du Raspberry Pi",
|
||||
"sysinfo_rpi_model_unknown": "Le modèle n'est pas un Raspberry Pi",
|
||||
"sysinfo_storage_free_space": "Stockage Disponible",
|
||||
"sysinfo_memory_usage": "Utilisation Mémoire",
|
||||
"sysinfo_os_version": "Version SE",
|
||||
|
||||
27
lang/it.json
27
lang/it.json
@ -59,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Sei sicuro?",
|
||||
"slideshow_content_page_title": "Libreria dei contenuti",
|
||||
"slideshow_content_button_add": "Nuovo Contenuto",
|
||||
"slideshow_content_referenced_in_slide_error": "Si fa riferimento al contenuto '%contentName%' in una diapositiva, rimuovere prima la diapositiva",
|
||||
"slideshow_content_referenced_in_slide_error": "Si fa riferimento al contenuto in una diapositiva, rimuovere prima la diapositiva",
|
||||
"slideshow_content_panel_active": "Contenuti",
|
||||
"slideshow_content_panel_empty": "Attualmente non ci sono contenuti. %link% adesso.",
|
||||
"slideshow_content_panel_th_name": "Nome",
|
||||
@ -256,27 +256,12 @@
|
||||
"common_apply": "Applica",
|
||||
"common_saved": "Le modifiche sono state salvate",
|
||||
"common_new_folder": "Nuovo Cartella",
|
||||
"common_folder_not_empty_error": "La cartella '%folderName%' non è vuota, devi prima eliminarne il contenuto",
|
||||
"common_folder_not_empty_error": "La cartella non è vuota, devi prima eliminarne il contenuto",
|
||||
"common_copied": "Elemento copiato!",
|
||||
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Si è verificato un errore %code%",
|
||||
"common_http_error_413": "I file sono troppo grandi",
|
||||
"common_width": "Larghezza",
|
||||
"common_height": "Altezza",
|
||||
"common_position": "Posizione",
|
||||
"common_angle": "Angolo",
|
||||
"common_size": "Dimensioni",
|
||||
"composition_elements_heading": "Elementi",
|
||||
"composition_element_add": "Aggiungi elemento",
|
||||
"composition_elements_delete_all": "Elimina tutto",
|
||||
"composition_presets": "Preimpostazioni",
|
||||
"composition_presets_grid_2x2": "Griglia 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Schermo",
|
||||
"composition_element_x_axis": "Asse X",
|
||||
"composition_element_y_axis": "Asse Y",
|
||||
"composition_element_match_content_aspect_ratio": "Regola la scala del contenuto",
|
||||
"logout": "Logout",
|
||||
"login_error_not_found": "Credenziali errate",
|
||||
"login_error_bad_credentials": "Credenziali errate",
|
||||
@ -308,10 +293,6 @@
|
||||
"enum_content_type_external_storage": "Archiviazione esterna",
|
||||
"enum_content_type_external_storage_object_label": "Specificare una directory esistente relativi al seguente percorso",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Percorso relativo ad un dispositivo rimovibile",
|
||||
"enum_content_type_composition": "Composizione",
|
||||
"enum_content_type_composition_object_label": "Rapporto di aspetto dello schermo",
|
||||
"enum_content_type_text": "Testo",
|
||||
"enum_content_type_text_object_label": "Testo visualizzato",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Immagine",
|
||||
@ -330,8 +311,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Altro",
|
||||
"sysinfo_device_model": "Modello del dispositivo",
|
||||
"sysinfo_device_model_unknown": "Modello sconosciuto",
|
||||
"sysinfo_rpi_model": "Raspberry Pi Model",
|
||||
"sysinfo_rpi_model_unknown": "Informazioni Raspberry Pi non disponibili",
|
||||
"sysinfo_storage_free_space": "Spazio libero",
|
||||
"sysinfo_memory_usage": "Memoria usata",
|
||||
"sysinfo_os_version": "OS Version",
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
|
||||
<div class="top-content">
|
||||
<div class="top-actions">
|
||||
|
||||
@ -31,9 +32,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
<div class="bottom-content">
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
@ -6,4 +6,3 @@ waitress
|
||||
flask-login
|
||||
pysqlite3
|
||||
psutil
|
||||
pymediainfo
|
||||
|
||||
56
setup.py
56
setup.py
@ -1,56 +0,0 @@
|
||||
# obscreen
|
||||
# ---------------
|
||||
# A fancy self-hosted digital signage tool. Free, simple and working.
|
||||
#
|
||||
# Author: jr-k (c) 2024
|
||||
# Website: https://github.com/jr-k/obscreen
|
||||
# License: GPLv2 (see LICENSE file)
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
common_dependencies = [
|
||||
'flask==2.3.3',
|
||||
'flask-restx==1.3.0',
|
||||
'python-dotenv',
|
||||
'cron-descriptor',
|
||||
'waitress',
|
||||
'flask-login',
|
||||
'psutil',
|
||||
'pymediainfo',
|
||||
'pysqlite3',
|
||||
]
|
||||
|
||||
if sys.platform == "win32":
|
||||
common_dependencies.remove('pysqlite3')
|
||||
|
||||
if sys.platform == "darwin":
|
||||
common_dependencies.remove('pysqlite3')
|
||||
|
||||
os.environ['PYTHONUTF8'] = '1'
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
|
||||
setup(
|
||||
name='obscreen',
|
||||
version=open('version.txt').read(),
|
||||
description='A fancy self-hosted digital signage tool. Free, simple and working.',
|
||||
long_description=open('README.md').read(),
|
||||
long_description_content_type='text/markdown',
|
||||
author='JRK',
|
||||
author_email='jrk@jierka.com',
|
||||
url='https://github.com/jr-k/obscreen',
|
||||
packages=find_packages(),
|
||||
platforms='any',
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3',
|
||||
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
|
||||
'Operating System :: OS Independent',
|
||||
'Topic :: Desktop Environment :: Screen Savers',
|
||||
'Topic :: Multimedia :: Graphics'
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
install_requires=common_dependencies,
|
||||
)
|
||||
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.User import User
|
||||
@ -26,6 +26,8 @@ class AuthController(ObController):
|
||||
self._app.add_url_rule('/auth/user/delete/<user_id>', 'auth_user_delete', self.guard_auth(self._auth(self.auth_user_delete)), methods=['GET'])
|
||||
|
||||
def login(self):
|
||||
login_error = None
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('playlist'))
|
||||
|
||||
@ -39,12 +41,13 @@ class AuthController(ObController):
|
||||
login_user(user)
|
||||
return redirect(url_for('playlist'))
|
||||
else:
|
||||
flash(self.t('login_error_bad_credentials'), 'error')
|
||||
login_error = 'bad_credentials'
|
||||
else:
|
||||
flash(self.t('login_error_not_found'), 'error')
|
||||
login_error = 'not_found'
|
||||
|
||||
return render_template(
|
||||
'auth/login.jinja.html',
|
||||
login_error=login_error,
|
||||
last_username=request.form['username'] if 'username' in request.form else None
|
||||
)
|
||||
|
||||
@ -64,6 +67,7 @@ class AuthController(ObController):
|
||||
|
||||
return render_template(
|
||||
'auth/list.jinja.html',
|
||||
error=request.args.get('error', None),
|
||||
users=self._model_store.user().get_users(exclude=User.DEFAULT_USER if demo else None),
|
||||
plugin_core_api_enabled=self._model_store.variable().map().get('plugin_core_api_enabled').as_bool()
|
||||
)
|
||||
@ -92,12 +96,10 @@ class AuthController(ObController):
|
||||
return redirect(url_for('auth_user_list'))
|
||||
|
||||
if user.id == str(current_user.id):
|
||||
flash(self.t('auth_user_delete_cant_delete_yourself'), 'error')
|
||||
return redirect(url_for('auth_user_list'))
|
||||
return redirect(url_for('auth_user_list', error='auth_user_delete_cant_delete_yourself'))
|
||||
|
||||
if self._model_store.user().count_all_enabled() == 1:
|
||||
flash(self.t('auth_user_delete_at_least_one_account'), 'error')
|
||||
return redirect(url_for('auth_user_list'))
|
||||
return redirect(url_for('auth_user_list', error='auth_user_delete_at_least_one_account'))
|
||||
|
||||
self._model_store.user().delete(user_id)
|
||||
return redirect(url_for('auth_user_list'))
|
||||
|
||||
@ -2,12 +2,11 @@ import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.Content import Content
|
||||
from src.model.enum.ContentType import ContentType
|
||||
from src.model.enum.ContentMetadata import ContentMetadata
|
||||
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import str_to_enum, get_optional_string
|
||||
@ -28,6 +27,7 @@ class ContentController(ObController):
|
||||
self._app.add_url_rule('/slideshow/content/move-folder', 'slideshow_content_folder_move', self._auth(self.slideshow_content_folder_move), methods=['POST'])
|
||||
self._app.add_url_rule('/slideshow/content/rename-folder', 'slideshow_content_folder_rename', self._auth(self.slideshow_content_folder_rename), methods=['POST'])
|
||||
self._app.add_url_rule('/slideshow/content/delete-folder', 'slideshow_content_folder_delete', self._auth(self.slideshow_content_folder_delete), methods=['GET'])
|
||||
self._app.add_url_rule('/slideshow/content/show/<content_id>', 'slideshow_content_show', self._auth(self.slideshow_content_show), methods=['GET'])
|
||||
self._app.add_url_rule('/slideshow/content/upload-bulk', 'slideshow_content_upload_bulk', self._auth(self.slideshow_content_upload_bulk), methods=['POST'])
|
||||
self._app.add_url_rule('/slideshow/content/delete-bulk-explr', 'slideshow_content_delete_bulk_explr', self._auth(self.slideshow_content_delete_bulk_explr), methods=['GET'])
|
||||
|
||||
@ -110,26 +110,15 @@ class ContentController(ObController):
|
||||
if not content:
|
||||
return abort(404)
|
||||
|
||||
vargs = {}
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
edit_view = 'slideshow/contents/edit.jinja.html'
|
||||
|
||||
if content.type == ContentType.COMPOSITION:
|
||||
edit_view = 'slideshow/contents/edit-composition.jinja.html'
|
||||
vargs['folders_tree'] = self._model_store.folder().get_folder_tree(FolderEntity.CONTENT)
|
||||
vargs['foldered_contents'] = self._model_store.content().get_all_indexed('folder_id', multiple=True)
|
||||
elif content.type == ContentType.TEXT:
|
||||
edit_view = 'slideshow/contents/edit-text.jinja.html'
|
||||
|
||||
return render_template(
|
||||
edit_view,
|
||||
'slideshow/contents/edit.jinja.html',
|
||||
content=content,
|
||||
working_folder_path=working_folder_path,
|
||||
working_folder=working_folder,
|
||||
enum_content_type=ContentType,
|
||||
enum_content_metadata=ContentMetadata,
|
||||
external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint'),
|
||||
**vargs
|
||||
external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint')
|
||||
)
|
||||
|
||||
def slideshow_content_save(self, content_id: int = 0):
|
||||
@ -146,19 +135,17 @@ class ContentController(ObController):
|
||||
)
|
||||
self._post_update()
|
||||
|
||||
flash(self.t('common_saved'), 'success')
|
||||
|
||||
return redirect(url_for('slideshow_content_edit', content_id=content_id))
|
||||
return redirect(url_for('slideshow_content_edit', content_id=content_id, saved=1))
|
||||
|
||||
def slideshow_content_delete(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
error = self.delete_content_by_id(request.args.get('id'))
|
||||
error_tuple = self.delete_content_by_id(request.args.get('id'))
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('slideshow_content_list', **route_args))
|
||||
|
||||
@ -238,16 +225,24 @@ class ContentController(ObController):
|
||||
|
||||
def slideshow_content_folder_delete(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
error = self.delete_folder_by_id(request.args.get('id'))
|
||||
error_tuple = self.delete_folder_by_id(request.args.get('id'))
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
|
||||
if error:
|
||||
flash(self.t(error), 'error')
|
||||
if error_tuple:
|
||||
route_args[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('slideshow_content_list', **route_args))
|
||||
|
||||
def slideshow_content_show(self, content_id: int = 0):
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
return abort(404)
|
||||
|
||||
return redirect(self._model_store.content().resolve_content_location(content))
|
||||
|
||||
def slideshow_content_delete_bulk_explr(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
entity_ids = request.args.get('entity_ids', '').split(',')
|
||||
@ -256,17 +251,17 @@ class ContentController(ObController):
|
||||
|
||||
for id in entity_ids:
|
||||
if id:
|
||||
error = self.delete_content_by_id(id)
|
||||
error_tuple = self.delete_content_by_id(id)
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
for id in folder_ids:
|
||||
if id:
|
||||
error = self.delete_folder_by_id(id)
|
||||
error_tuple = self.delete_folder_by_id(id)
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('slideshow_content_list', **route_args_dict))
|
||||
|
||||
@ -277,7 +272,7 @@ class ContentController(ObController):
|
||||
return None
|
||||
|
||||
if self._model_store.slide().count_slides_for_content(content.id) > 0:
|
||||
return 'slideshow_content_referenced_in_slide_error'.replace('%contentName%', content.name)
|
||||
return 'referenced_in_slide_error', content.name
|
||||
|
||||
self._model_store.content().delete(content.id)
|
||||
self._post_update()
|
||||
@ -293,7 +288,7 @@ class ContentController(ObController):
|
||||
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
|
||||
|
||||
if content_counter > 0 or folder_counter:
|
||||
return self.t('common_folder_not_empty_error').replace('%folderName%', folder.name)
|
||||
return 'folder_not_empty_error', folder.name
|
||||
|
||||
self._model_store.folder().delete(id=folder.id)
|
||||
self._post_update()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.NodePlayer import NodePlayer
|
||||
from src.interface.ObController import ObController
|
||||
@ -108,19 +108,18 @@ class FleetNodePlayerController(ObController):
|
||||
)
|
||||
self._post_update()
|
||||
|
||||
flash(self.t('common_saved'), 'success')
|
||||
|
||||
# return redirect(url_for('fleet_node_player_edit', node_player_id=node_player_id, saved=1))
|
||||
return redirect(url_for('fleet_node_player_list', path=working_folder_path))
|
||||
|
||||
def fleet_node_player_delete(self):
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
error = self.delete_node_player_by_id(request.args.get('id'))
|
||||
error_tuple = self.delete_node_player_by_id(request.args.get('id'))
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('fleet_node_player_list', **route_args))
|
||||
|
||||
@ -201,13 +200,13 @@ class FleetNodePlayerController(ObController):
|
||||
|
||||
def fleet_node_player_folder_delete(self):
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
error = self.delete_folder_by_id(request.args.get('id'))
|
||||
error_tuple = self.delete_folder_by_id(request.args.get('id'))
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('fleet_node_player_list', **route_args))
|
||||
|
||||
@ -219,17 +218,17 @@ class FleetNodePlayerController(ObController):
|
||||
|
||||
for id in entity_ids:
|
||||
if id:
|
||||
error = self.delete_node_player_by_id(id)
|
||||
error_tuple = self.delete_node_player_by_id(id)
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
for id in folder_ids:
|
||||
if id:
|
||||
error = self.delete_folder_by_id(id)
|
||||
error_tuple = self.delete_folder_by_id(id)
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('fleet_node_player_list', **route_args_dict))
|
||||
|
||||
@ -253,7 +252,7 @@ class FleetNodePlayerController(ObController):
|
||||
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
|
||||
|
||||
if node_player_counter > 0 or folder_counter:
|
||||
return self.t('common_folder_not_empty_error').replace('%folderName%', folder.name)
|
||||
return 'folder_not_empty_error', folder.name
|
||||
|
||||
self._model_store.folder().delete(id=folder.id)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.NodePlayerGroup import NodePlayerGroup
|
||||
from src.model.enum.FolderEntity import FolderEntity
|
||||
@ -43,6 +43,7 @@ class FleetNodePlayerGroupController(ObController):
|
||||
|
||||
return render_template(
|
||||
'fleet/player-group/list.jinja.html',
|
||||
error=request.args.get('error', None),
|
||||
current_player_group=current_player_group,
|
||||
node_player_groups=node_player_groups,
|
||||
pcounters=pcounters,
|
||||
@ -85,8 +86,7 @@ class FleetNodePlayerGroupController(ObController):
|
||||
|
||||
def fleet_node_player_group_delete(self, player_group_id: int):
|
||||
if self._model_store.node_player().count_node_players_for_group(player_group_id) > 0:
|
||||
flash(self.t('node_player_group_delete_has_node_player'), 'error')
|
||||
return redirect(url_for('fleet_node_player_group_list', player_group_id=player_group_id))
|
||||
return redirect(url_for('fleet_node_player_group_list', player_group_id=player_group_id, error='node_player_group_delete_has_node_player'))
|
||||
|
||||
self._model_store.node_player_group().delete(player_group_id)
|
||||
return redirect(url_for('fleet_node_player_group'))
|
||||
|
||||
@ -28,31 +28,28 @@ class PlayerController(ObController):
|
||||
self._app.add_url_rule('/player/playlist', 'player_playlist', self.player_playlist, methods=['GET'])
|
||||
self._app.add_url_rule('/player/playlist/use/<playlist_slug_or_id>', 'player_playlist_use', self.player_playlist, methods=['GET'])
|
||||
self._app.add_url_rule('/serve/content/<content_type>/<content_id>/<content_location>', 'serve_content_file', self.serve_content_file, methods=['GET'])
|
||||
self._app.add_url_rule('/serve/content/composition/<content_id>', 'serve_content_composition', self.serve_content_composition, methods=['GET'])
|
||||
|
||||
def player(self, playlist_slug_or_id: str = ''):
|
||||
preview_playlist = request.args.get('preview_playlist')
|
||||
preview_content_id = request.args.get('preview_content_id')
|
||||
playlist_id = None
|
||||
playlist_slug_or_id = self._get_dynamic_playlist_id(playlist_slug_or_id)
|
||||
|
||||
if not preview_content_id:
|
||||
query = " (slug = ? OR id = ?) "
|
||||
query_args = {
|
||||
"slug": playlist_slug_or_id,
|
||||
"id": playlist_slug_or_id,
|
||||
}
|
||||
query = " (slug = ? OR id = ?) "
|
||||
query_args = {
|
||||
"slug": playlist_slug_or_id,
|
||||
"id": playlist_slug_or_id,
|
||||
}
|
||||
|
||||
if not preview_playlist:
|
||||
query = query + " AND enabled = ? "
|
||||
query_args["enabled"] = True
|
||||
if not preview_playlist:
|
||||
query = query + " AND enabled = ? "
|
||||
query_args["enabled"] = True
|
||||
|
||||
current_playlist = self._model_store.playlist().get_one_by(query, query_args)
|
||||
current_playlist = self._model_store.playlist().get_one_by(query, query_args)
|
||||
|
||||
if playlist_slug_or_id and not current_playlist:
|
||||
return abort(404)
|
||||
if playlist_slug_or_id and not current_playlist:
|
||||
return abort(404)
|
||||
|
||||
playlist_id = current_playlist.id if current_playlist else None
|
||||
playlist_id = current_playlist.id if current_playlist else None
|
||||
|
||||
try:
|
||||
items = self._get_playlist(playlist_id=playlist_id, preview_content_id=preview_content_id)
|
||||
@ -66,8 +63,6 @@ class PlayerController(ObController):
|
||||
slide_animation_entrance_effect = request.args.get('animation_effect', self._model_store.variable().get_one_by_name('slide_animation_entrance_effect').eval())
|
||||
slide_animation_exit_effect = request.args.get('slide_animation_exit_effect', self._model_store.variable().get_one_by_name('slide_animation_exit_effect').eval())
|
||||
|
||||
|
||||
|
||||
return render_template(
|
||||
'player/player.jinja.html',
|
||||
items=items,
|
||||
@ -86,8 +81,7 @@ class PlayerController(ObController):
|
||||
interfaces=[iface['ip_address'] for iface in get_network_interfaces()],
|
||||
external_url=self._model_store.variable().get_one_by_name('external_url').as_string().strip(),
|
||||
time_with_seconds=self._model_store.variable().get_one_by_name('default_slide_time_with_seconds'),
|
||||
noplaylist=request.args.get('noplaylist', '0') == '1',
|
||||
hard_refresh_request=self._model_store.variable().get_one_by_name("refresh_player_request").as_int()
|
||||
noplaylist=request.args.get('noplaylist', '0') == '1'
|
||||
)
|
||||
|
||||
def player_playlist(self, playlist_slug_or_id: str = ''):
|
||||
@ -127,7 +121,7 @@ class PlayerController(ObController):
|
||||
preview_content = self._model_store.content().get(preview_content_id) if preview_content_id else None
|
||||
preview_mode = preview_content is not None
|
||||
|
||||
if not preview_mode and (playlist_id == 0 or not playlist_id):
|
||||
if playlist_id == 0 or not playlist_id:
|
||||
playlist = self._model_store.playlist().get_one_by(query="fallback = 1")
|
||||
|
||||
if playlist:
|
||||
@ -137,9 +131,8 @@ class PlayerController(ObController):
|
||||
|
||||
enabled_slides = [Slide(content_id=preview_content.id, duration=1000000)] if preview_mode else self._model_store.slide().get_slides(enabled=True, playlist_id=playlist_id)
|
||||
slides = self._model_store.slide().to_dict(enabled_slides)
|
||||
content_ids = [str(slide['content_id']) for slide in slides if slide['content_id'] is not None]
|
||||
contents = self._model_store.content().get_all_indexed(query="id IN ({})".format(','.join(content_ids)))
|
||||
playlist = self._model_store.playlist().get(playlist_id) if not preview_mode else None
|
||||
contents = self._model_store.content().get_all_indexed()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
position = 9999
|
||||
|
||||
playlist_loop = []
|
||||
@ -254,14 +247,3 @@ class PlayerController(ObController):
|
||||
response.headers['ETag'] = etag
|
||||
|
||||
return response
|
||||
|
||||
def serve_content_composition(self, content_id):
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content or content.type != ContentType.COMPOSITION:
|
||||
abort(404, 'Content not found')
|
||||
|
||||
return render_template(
|
||||
'player/content/composition.jinja.html',
|
||||
content=content,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.model.enum.FolderEntity import FolderEntity
|
||||
@ -35,6 +35,7 @@ class PlaylistController(ObController):
|
||||
|
||||
return render_template(
|
||||
'playlist/list.jinja.html',
|
||||
error=request.args.get('error', None),
|
||||
current_playlist=current_playlist,
|
||||
playlists=playlists,
|
||||
durations=durations,
|
||||
@ -54,8 +55,7 @@ class PlaylistController(ObController):
|
||||
playlist = Playlist(
|
||||
name=request.form['name'],
|
||||
enabled=True,
|
||||
time_sync=False,
|
||||
fallback=self._model_store.playlist().count_fallbacks() == 0
|
||||
time_sync=False
|
||||
)
|
||||
|
||||
try:
|
||||
@ -70,8 +70,7 @@ class PlaylistController(ObController):
|
||||
id=request.form['id'],
|
||||
name=request.form['name'],
|
||||
time_sync=True if 'time_sync' in request.form else False,
|
||||
enabled=True if 'enabled' in request.form else False,
|
||||
fallback=True if self._model_store.playlist().count_fallbacks() == 0 else None
|
||||
enabled=True if 'enabled' in request.form else False
|
||||
)
|
||||
return redirect(url_for('playlist_list', playlist_id=request.form['id']))
|
||||
|
||||
@ -82,12 +81,10 @@ class PlaylistController(ObController):
|
||||
abort(404)
|
||||
|
||||
if self._model_store.slide().count_slides_for_playlist(playlist_id) > 0:
|
||||
flash(self.t('playlist_delete_has_slides'), 'error')
|
||||
return redirect(url_for('playlist_list', playlist_id=playlist_id))
|
||||
return redirect(url_for('playlist_list', playlist_id=playlist_id, error='playlist_delete_has_slides'))
|
||||
|
||||
if self._model_store.node_player_group().count_node_player_groups_for_playlist(playlist_id) > 0:
|
||||
flash(self.t('playlist_delete_has_node_player_groups'), 'error')
|
||||
return redirect(url_for('playlist_list', playlist_id=playlist_id))
|
||||
return redirect(url_for('playlist_list', playlist_id=playlist_id, error='playlist_delete_has_node_player_groups'))
|
||||
|
||||
self._model_store.playlist().delete(playlist_id)
|
||||
return redirect(url_for('playlist'))
|
||||
|
||||
@ -2,7 +2,7 @@ import time
|
||||
import json
|
||||
import threading
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for
|
||||
from typing import Optional
|
||||
|
||||
from src.service.ModelStore import ModelStore
|
||||
@ -40,8 +40,7 @@ class SettingsController(ObController):
|
||||
error = self._pre_update(request.form['id'])
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
return redirect(url_for('settings_variable_list'))
|
||||
return redirect(url_for('settings_variable_list', error=error))
|
||||
|
||||
self._model_store.variable().update_form(request.form['id'], request.form['value'])
|
||||
redirect_response = self._post_update(request.form['id'])
|
||||
@ -55,8 +54,7 @@ class SettingsController(ObController):
|
||||
error = self._pre_update(request.form['id'])
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
return redirect(url_for('settings_variable_plugin_list'))
|
||||
return redirect(url_for('settings_variable_plugin_list', error=error))
|
||||
|
||||
self._model_store.variable().update_form(request.form['id'], request.form['value'])
|
||||
redirect_response = self._post_update(request.form['id'])
|
||||
@ -81,8 +79,7 @@ class SettingsController(ObController):
|
||||
|
||||
if variable.name == 'slide_upload_limit':
|
||||
self.reload_web_server()
|
||||
flash(self.t('common_restart_needed'), 'warning')
|
||||
return redirect(url_for('settings_variable_list'))
|
||||
return redirect(url_for('settings_variable_list', warning='common_restart_needed'))
|
||||
|
||||
if variable.name == 'fleet_player_enabled':
|
||||
self.reload_web_server()
|
||||
@ -101,10 +98,7 @@ class SettingsController(ObController):
|
||||
thread = threading.Thread(target=self.plugin_update)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
flash(self.t('common_restart_needed'), 'warning')
|
||||
return redirect(url_for('settings_variable_plugin_list'))
|
||||
|
||||
flash(self.t('common_saved'), 'success')
|
||||
return redirect(url_for('settings_variable_plugin_list', warning='common_restart_needed'))
|
||||
|
||||
def plugin_update(self) -> None:
|
||||
restart()
|
||||
|
||||
@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.Slide import Slide
|
||||
@ -102,11 +102,11 @@ class SlideController(ObController):
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
def slideshow_player_refresh(self):
|
||||
referrer_path = self.get_referrer_path()
|
||||
max_timeout_value = self._model_store.variable().get_one_by_name('polling_interval').as_string()
|
||||
flash(self.t('slideshow_slide_refresh_player_success').replace('%time%', max_timeout_value), 'success:refresh')
|
||||
self._model_store.variable().update_by_name("refresh_player_request", time.time())
|
||||
return redirect(referrer_path)
|
||||
max_timeout_value = self._model_store.variable().get_one_by_name('polling_interval').as_int()
|
||||
query_params = '{}={}'.format('refresh_player', max_timeout_value)
|
||||
next_url = request.args.get('next')
|
||||
return redirect('{}{}{}'.format(next_url, '&' if '?' in next_url else '?', query_params))
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
||||
|
||||
@ -69,6 +69,5 @@ class SysinfoController(ObController):
|
||||
def sysinfo_get_ipaddr(self):
|
||||
return jsonify({
|
||||
'external_url': self._model_store.variable().get_one_by_name('external_url').as_string().strip(),
|
||||
'interfaces': [iface['ip_address'] for iface in get_network_interfaces()],
|
||||
'hard_refresh_request': self._model_store.variable().get_one_by_name("refresh_player_request").as_int()
|
||||
'interfaces': [iface['ip_address'] for iface in get_network_interfaces()]
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import abc
|
||||
|
||||
from typing import Optional, List, Dict, Union
|
||||
from flask import request
|
||||
from src.service.TemplateRenderer import TemplateRenderer
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.interface.ObPlugin import ObPlugin
|
||||
@ -52,19 +51,3 @@ class ObController(abc.ABC):
|
||||
|
||||
def api(self):
|
||||
return self._web_server.api
|
||||
|
||||
def get_referrer_path(self):
|
||||
referer_url = request.referrer
|
||||
if referer_url:
|
||||
return '/' + referer_url.replace(request.host_url, '').split('?')[0]
|
||||
return None
|
||||
|
||||
def get_referrer_rule(self):
|
||||
referer_path = self.get_referrer_path()
|
||||
|
||||
if referer_path:
|
||||
for rule in self._app.url_map.iter_rules():
|
||||
if referer_path == rule.rule.split('/<')[0]:
|
||||
return rule.rule
|
||||
|
||||
return None
|
||||
|
||||
@ -6,7 +6,6 @@ from flask import url_for
|
||||
|
||||
from src.model.entity.Content import Content
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.model.enum.ContentMetadata import ContentMetadata
|
||||
from src.model.enum.ContentType import ContentType
|
||||
from src.util.utils import get_yt_video_id
|
||||
from src.manager.DatabaseManager import DatabaseManager
|
||||
@ -17,8 +16,7 @@ from src.manager.VariableManager import VariableManager
|
||||
from src.service.ModelManager import ModelManager
|
||||
from src.util.UtilFile import randomize_filename
|
||||
from src.util.UtilNetwork import get_preferred_ip_address
|
||||
from src.util.UtilVideo import get_video_metadata
|
||||
from src.util.UtilPicture import get_picture_metadata
|
||||
from src.util.UtilVideo import mp4_duration_with_ffprobe
|
||||
from src.util.utils import encode_uri_component
|
||||
|
||||
|
||||
@ -31,7 +29,6 @@ class ContentManager(ModelManager):
|
||||
"type CHAR(30)",
|
||||
"location TEXT",
|
||||
"duration FLOAT",
|
||||
"metadata TEXT",
|
||||
"folder_id INTEGER",
|
||||
"created_by CHAR(255)",
|
||||
"updated_by CHAR(255)",
|
||||
@ -43,12 +40,6 @@ class ContentManager(ModelManager):
|
||||
super().__init__(lang_manager, database_manager, user_manager, variable_manager)
|
||||
self._config_manager = config_manager
|
||||
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
|
||||
self.pre_migrate()
|
||||
|
||||
def pre_migrate(self):
|
||||
if not self._variable_manager.get_one_by_name('refresh_all_metadata').as_bool():
|
||||
self.refresh_all_metadata()
|
||||
self._variable_manager.update_by_name('refresh_all_metadata', True)
|
||||
|
||||
def hydrate_object(self, raw_content: dict, id: int = None) -> Content:
|
||||
if id:
|
||||
@ -82,12 +73,10 @@ class ContentManager(ModelManager):
|
||||
def get_all(self, sort: Optional[str] = 'created_at', ascending=False) -> List[Content]:
|
||||
return self.hydrate_list(self._db.get_all(table_name=self.TABLE_NAME, sort=sort, ascending=ascending))
|
||||
|
||||
def get_all_indexed(self, attribute: str = 'id', multiple=False, query: str = None) -> Dict[str, Content]:
|
||||
def get_all_indexed(self, attribute: str = 'id', multiple=False) -> Dict[str, Content]:
|
||||
index = {}
|
||||
|
||||
items = self.get_by(query) if query else self.get_contents()
|
||||
|
||||
for item in items:
|
||||
for item in self.get_contents():
|
||||
id = getattr(item, attribute)
|
||||
if multiple:
|
||||
if id not in index:
|
||||
@ -143,15 +132,14 @@ class ContentManager(ModelManager):
|
||||
def post_delete(self, content_id: str) -> str:
|
||||
return content_id
|
||||
|
||||
def update_form(self, id: int, name: Optional[str] = None, location: Optional[str] = None, metadata: Optional[str] = None) -> Optional[Content]:
|
||||
def update_form(self, id: int, name: str, location: Optional[str] = None) -> Optional[Content]:
|
||||
content = self.get(id)
|
||||
|
||||
if not content:
|
||||
return
|
||||
|
||||
form = {
|
||||
"name": name if isinstance(name, str) else content.name,
|
||||
"metadata": metadata if isinstance(metadata, str) else content.metadata
|
||||
"name": name,
|
||||
}
|
||||
|
||||
if location is not None and location:
|
||||
@ -210,29 +198,16 @@ class ContentManager(ModelManager):
|
||||
object_path = os.path.join(upload_dir, object_name)
|
||||
object.save(object_path)
|
||||
content.location = object_path
|
||||
self.set_metadata(content)
|
||||
|
||||
if type == ContentType.VIDEO:
|
||||
content.duration = mp4_duration_with_ffprobe(content.location)
|
||||
|
||||
else:
|
||||
content.location = ContentType.get_initial_location(content.type, location)
|
||||
content.location = location if location else ''
|
||||
|
||||
self.add_form(content)
|
||||
return self.get_one_by(query="uuid = '{}'".format(content.uuid))
|
||||
|
||||
def set_metadata(self, content: Content) -> str:
|
||||
if content.type == ContentType.VIDEO:
|
||||
width, height, duration = get_video_metadata(content.location)
|
||||
content.duration = duration
|
||||
content.set_metadata(ContentMetadata.DURATION, duration)
|
||||
content.set_metadata(ContentMetadata.WIDTH, width)
|
||||
content.set_metadata(ContentMetadata.HEIGHT, height)
|
||||
elif content.type == ContentType.PICTURE:
|
||||
width, height = get_picture_metadata(content.location)
|
||||
content.set_metadata(ContentMetadata.WIDTH, width)
|
||||
content.set_metadata(ContentMetadata.HEIGHT, height)
|
||||
else:
|
||||
content.init_metadata()
|
||||
|
||||
return content.metadata
|
||||
|
||||
def delete(self, id: int) -> None:
|
||||
content = self.get(id)
|
||||
|
||||
@ -261,17 +236,7 @@ class ContentManager(ModelManager):
|
||||
location = content.location
|
||||
|
||||
if content.type == ContentType.YOUTUBE:
|
||||
location = content.location
|
||||
elif content.type == ContentType.TEXT:
|
||||
pass
|
||||
elif content.type == ContentType.COMPOSITION:
|
||||
location = "{}/{}".format(
|
||||
var_external_url if len(var_external_url) > 0 else "",
|
||||
url_for(
|
||||
'serve_content_composition',
|
||||
content_id=content.id
|
||||
).strip('/')
|
||||
)
|
||||
location = "https://www.youtube.com/watch?v={}".format(content.location)
|
||||
elif content.has_file() or content.type == ContentType.EXTERNAL_STORAGE:
|
||||
location = "{}/{}".format(
|
||||
var_external_url if len(var_external_url) > 0 else "",
|
||||
@ -283,13 +248,6 @@ class ContentManager(ModelManager):
|
||||
).strip('/')
|
||||
)
|
||||
elif content.type == ContentType.URL:
|
||||
location = 'http://' + content.location if content.location and not content.location.startswith('http') else content.location
|
||||
location = 'http://' + content.location if not content.location.startswith('http') else content.location
|
||||
|
||||
return location
|
||||
|
||||
def refresh_all_metadata(self):
|
||||
for content in self.get_all():
|
||||
self.update_form(
|
||||
id=content.id,
|
||||
metadata=self.set_metadata(content)
|
||||
)
|
||||
return location
|
||||
@ -163,7 +163,7 @@ GROUP BY playlist_id;
|
||||
def post_delete(self, playlist_id: str) -> str:
|
||||
return playlist_id
|
||||
|
||||
def update_form(self, id: int, name: Optional[str] = None, time_sync: Optional[bool] = None, enabled: Optional[bool] = None, fallback: Optional[bool] = None) -> None:
|
||||
def update_form(self, id: int, name: Optional[str] = None, time_sync: Optional[bool] = None, enabled: Optional[bool] = None) -> None:
|
||||
playlist = self.get(id)
|
||||
|
||||
if not playlist:
|
||||
@ -173,7 +173,6 @@ GROUP BY playlist_id;
|
||||
"name": name if isinstance(name, str) else playlist.name,
|
||||
"time_sync": time_sync if isinstance(time_sync, bool) else playlist.time_sync,
|
||||
"enabled": enabled if isinstance(enabled, bool) else playlist.enabled,
|
||||
"fallback": fallback if isinstance(fallback, bool) else playlist.fallback,
|
||||
}
|
||||
|
||||
if name != playlist.name:
|
||||
@ -187,7 +186,7 @@ GROUP BY playlist_id;
|
||||
self.post_update(id)
|
||||
|
||||
def check_and_set_fallback(self):
|
||||
if self.count_fallbacks() == 0:
|
||||
if len(self.get_by("fallback = 1")) == 0:
|
||||
self.set_fallback()
|
||||
|
||||
def set_fallback(self, playlist_id: Optional[int] = 0) -> None:
|
||||
@ -227,8 +226,3 @@ GROUP BY playlist_id;
|
||||
def to_dict(self, playlists: List[Playlist]) -> List[Dict]:
|
||||
return [playlist.to_dict() for playlist in playlists]
|
||||
|
||||
def count_all(self):
|
||||
return len(self.get_all())
|
||||
|
||||
def count_fallbacks(self):
|
||||
return len(self.get_by("fallback = 1"))
|
||||
|
||||
@ -218,14 +218,13 @@ class UserManager:
|
||||
user_id = self.get_logged_user("id")
|
||||
now = time.time()
|
||||
|
||||
if user_id:
|
||||
if 'created_by' not in object or not object['created_by']:
|
||||
object["created_by"] = user_id
|
||||
edits['created_by'] = object['created_by']
|
||||
if 'created_by' not in object or not object['created_by']:
|
||||
object["created_by"] = user_id
|
||||
edits['created_by'] = object['created_by']
|
||||
|
||||
if 'updated_by' not in object or not object['updated_by']:
|
||||
object["updated_by"] = user_id
|
||||
edits['updated_by'] = object['updated_by']
|
||||
if 'updated_by' not in object or not object['updated_by']:
|
||||
object["updated_by"] = user_id
|
||||
edits['updated_by'] = object['updated_by']
|
||||
|
||||
if 'created_at' not in object or not object['created_at']:
|
||||
object["created_at"] = now
|
||||
|
||||
@ -149,7 +149,6 @@ class VariableManager:
|
||||
{"name": "last_restart", "value": time.time(), "type": VariableType.TIMESTAMP, "editable": False, "description": self.t('settings_variable_desc_ro_editable')},
|
||||
{"name": "last_slide_update", "value": time.time(), "type": VariableType.TIMESTAMP, "editable": False, "description": self.t('settings_variable_desc_ro_last_slide_update')},
|
||||
{"name": "refresh_player_request", "value": time.time(), "type": VariableType.TIMESTAMP, "editable": False, "description": self.t('settings_variable_desc_ro_refresh_player_request')},
|
||||
{"name": "refresh_all_metadata", "value": False, "type": VariableType.BOOL, "editable": False, "description": None},
|
||||
]
|
||||
|
||||
for default_var in default_vars:
|
||||
|
||||
@ -4,17 +4,15 @@ import uuid
|
||||
|
||||
from typing import Optional, Union
|
||||
from src.model.enum.ContentType import ContentType, ContentInputType
|
||||
from src.model.enum.ContentMetadata import ContentMetadata
|
||||
from src.util.utils import str_to_enum
|
||||
|
||||
|
||||
class Content:
|
||||
|
||||
def __init__(self, uuid: str = '', location: str = '', metadata: str = '', type: Union[ContentType, str] = ContentType.URL, name: str = 'Untitled', id: Optional[int] = None, duration: Optional[float] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None, folder_id: Optional[int] = None):
|
||||
def __init__(self, uuid: str = '', location: str = '', type: Union[ContentType, str] = ContentType.URL, name: str = 'Untitled', id: Optional[int] = None, duration: Optional[float] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None, folder_id: Optional[int] = None):
|
||||
self._uuid = uuid if uuid else self.generate_and_set_uuid()
|
||||
self._id = id if id else None
|
||||
self._location = location
|
||||
self._metadata = metadata if metadata else self.init_metadata()
|
||||
self._type = str_to_enum(type, ContentType) if isinstance(type, str) else type
|
||||
self._name = name
|
||||
self._folder_id = folder_id
|
||||
@ -41,14 +39,6 @@ class Content:
|
||||
def uuid(self, value: str):
|
||||
self._uuid = value
|
||||
|
||||
@property
|
||||
def metadata(self) -> str:
|
||||
return self._metadata
|
||||
|
||||
@metadata.setter
|
||||
def metadata(self, value: str):
|
||||
self._metadata = value
|
||||
|
||||
@property
|
||||
def location(self) -> str:
|
||||
return self._location
|
||||
@ -134,7 +124,6 @@ class Content:
|
||||
f"updated_at='{self.updated_at}',\n" \
|
||||
f"folder_id='{self.folder_id}',\n" \
|
||||
f"duration='{self.duration}',\n" \
|
||||
f"metadata='{self.metadata}',\n" \
|
||||
f")"
|
||||
|
||||
def to_json(self, edits: dict = {}) -> str:
|
||||
@ -158,7 +147,6 @@ class Content:
|
||||
"updated_at": self.updated_at,
|
||||
"folder_id": self.folder_id,
|
||||
"duration": self.duration,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
if with_virtual:
|
||||
@ -177,32 +165,3 @@ class Content:
|
||||
|
||||
def is_editable(self) -> bool:
|
||||
return ContentInputType.is_editable(self.get_input_type())
|
||||
|
||||
def init_metadata(self):
|
||||
self.metadata = '{}'
|
||||
return self.metadata
|
||||
|
||||
def get_metadata(self, key: ContentMetadata, default=''):
|
||||
if not self.metadata:
|
||||
self.init_metadata()
|
||||
|
||||
metadata_obj = json.loads(self.metadata)
|
||||
return metadata_obj.get(key.value, default)
|
||||
|
||||
def set_metadata(self, key: ContentMetadata, value=None):
|
||||
if not self.metadata:
|
||||
self.init_metadata()
|
||||
|
||||
metadata_obj = json.loads(self.metadata)
|
||||
metadata_obj[key.value] = value
|
||||
self.metadata = json.dumps(metadata_obj)
|
||||
|
||||
def clear_metadata(self, key: ContentMetadata):
|
||||
if not self.metadata:
|
||||
self.init_metadata()
|
||||
|
||||
metadata_obj = json.loads(self.metadata)
|
||||
|
||||
if key.value in metadata_obj:
|
||||
del metadata_obj[key.value]
|
||||
self.metadata = json.dumps(metadata_obj)
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ContentMetadata(Enum):
|
||||
|
||||
DURATION = 'duration'
|
||||
WIDTH = 'width'
|
||||
HEIGHT = 'height'
|
||||
@ -1,4 +1,3 @@
|
||||
import json
|
||||
import mimetypes
|
||||
|
||||
from enum import Enum
|
||||
@ -11,9 +10,7 @@ class ContentInputType(Enum):
|
||||
|
||||
UPLOAD = 'upload'
|
||||
TEXT = 'text'
|
||||
HIDDEN = 'hidden'
|
||||
STORAGE = 'storage'
|
||||
COMPOSITION = 'composition'
|
||||
|
||||
@staticmethod
|
||||
def is_editable(value: Enum) -> bool:
|
||||
@ -23,8 +20,6 @@ class ContentInputType(Enum):
|
||||
return True
|
||||
elif value == ContentInputType.STORAGE:
|
||||
return True
|
||||
elif value == ContentInputType.COMPOSITION:
|
||||
return True
|
||||
|
||||
|
||||
class ContentType(Enum):
|
||||
@ -34,8 +29,6 @@ class ContentType(Enum):
|
||||
YOUTUBE = 'youtube'
|
||||
VIDEO = 'video'
|
||||
EXTERNAL_STORAGE = 'external_storage'
|
||||
COMPOSITION = 'composition'
|
||||
TEXT = 'text'
|
||||
|
||||
@staticmethod
|
||||
def guess_content_type_file(filename: str):
|
||||
@ -68,10 +61,6 @@ class ContentType(Enum):
|
||||
return ContentInputType.TEXT
|
||||
elif value == ContentType.EXTERNAL_STORAGE:
|
||||
return ContentInputType.STORAGE
|
||||
elif value == ContentType.COMPOSITION:
|
||||
return ContentInputType.COMPOSITION
|
||||
elif value == ContentType.TEXT:
|
||||
return ContentInputType.TEXT
|
||||
|
||||
@staticmethod
|
||||
def get_fa_icon(value: Union[Enum, str]) -> str:
|
||||
@ -88,10 +77,6 @@ class ContentType(Enum):
|
||||
return 'fa-link'
|
||||
elif value == ContentType.EXTERNAL_STORAGE:
|
||||
return 'fa-brands fa-usb'
|
||||
elif value == ContentType.COMPOSITION:
|
||||
return 'fa-solid fa-clone'
|
||||
elif value == ContentType.TEXT:
|
||||
return 'fa-solid fa-font'
|
||||
|
||||
return 'fa-file'
|
||||
|
||||
@ -110,39 +95,5 @@ class ContentType(Enum):
|
||||
return 'danger'
|
||||
elif value == ContentType.EXTERNAL_STORAGE:
|
||||
return 'other'
|
||||
elif value == ContentType.COMPOSITION:
|
||||
return 'purple'
|
||||
elif value == ContentType.TEXT:
|
||||
return 'gscaleF'
|
||||
|
||||
return 'neutral'
|
||||
|
||||
@staticmethod
|
||||
def get_initial_location(value: Enum, location: Optional[str] = None) -> str:
|
||||
if isinstance(value, str):
|
||||
value = str_to_enum(value, ContentType)
|
||||
|
||||
if value == ContentType.COMPOSITION:
|
||||
return json.dumps({
|
||||
"ratio": location if location else '16/9',
|
||||
"layers": {}
|
||||
})
|
||||
elif value == ContentType.TEXT:
|
||||
return json.dumps({
|
||||
"textLabel": location if location else 'Hello',
|
||||
"fontSize": 20,
|
||||
"color": '#FFFFFFFF',
|
||||
"fontFamily": "Arial",
|
||||
"fontBold": None,
|
||||
"fontItalic": None,
|
||||
"fontUnderline": None,
|
||||
"textAlign": "center",
|
||||
"backgroundColor": '#000000FF',
|
||||
"scrollEnable": False,
|
||||
"scrollDirection": "left",
|
||||
"scrollSpeed": "10",
|
||||
"singleLine": False,
|
||||
"margin": 0
|
||||
})
|
||||
|
||||
return location
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
try:
|
||||
import psutil
|
||||
except:
|
||||
pass
|
||||
|
||||
import os
|
||||
import platform
|
||||
import psutil
|
||||
import socket
|
||||
|
||||
from src.util.utils import get_working_directory
|
||||
@ -122,7 +118,7 @@ def get_network_ipaddr():
|
||||
def get_all_sysinfo():
|
||||
rpi_model = get_rpi_model()
|
||||
infos = {
|
||||
"sysinfo_device_model": rpi_model if rpi_model else 'sysinfo_device_model_unknown',
|
||||
"sysinfo_rpi_model": rpi_model if rpi_model else 'sysinfo_rpi_model_unknown',
|
||||
"sysinfo_storage_free_space": get_free_space(),
|
||||
"sysinfo_memory_usage": "{}{}".format(get_memory_usage()['percent'], "%"),
|
||||
"sysinfo_os_version": get_os_version(),
|
||||
|
||||
@ -39,9 +39,9 @@ class TemplateRenderer:
|
||||
AUTH_ENABLED=self._model_store.variable().map().get('auth_enabled').as_bool(),
|
||||
last_pillmenu_slideshow=self._model_store.variable().map().get('last_pillmenu_slideshow').as_string(),
|
||||
last_pillmenu_configuration=self._model_store.variable().map().get('last_pillmenu_configuration').as_string(),
|
||||
external_url=self._model_store.variable().map().get('external_url').as_string().strip('/'),
|
||||
last_pillmenu_fleet=self._model_store.variable().map().get('last_pillmenu_fleet').as_string(),
|
||||
last_pillmenu_security=self._model_store.variable().map().get('last_pillmenu_security').as_string(),
|
||||
external_url=self._model_store.variable().map().get('external_url').as_string(),
|
||||
track_created=self._model_store.user().track_user_created,
|
||||
track_updated=self._model_store.user().track_user_updated,
|
||||
PORT=self._model_store.config().map().get('port'),
|
||||
@ -54,7 +54,6 @@ class TemplateRenderer:
|
||||
is_cron_in_datetime_moment=is_cron_in_datetime_moment,
|
||||
is_cron_in_week_moment=is_cron_in_week_moment,
|
||||
json_dumps=json.dumps,
|
||||
json_loads=json.loads,
|
||||
merge_dicts=merge_dicts,
|
||||
dictsort=dictsort,
|
||||
truncate=truncate,
|
||||
|
||||
@ -50,6 +50,7 @@ class WebServer:
|
||||
host=self._model_store.config().map().get('bind'),
|
||||
port=self._model_store.config().map().get('port'),
|
||||
threads=100,
|
||||
max_request_body_size=self.get_max_upload_size(),
|
||||
)
|
||||
|
||||
def reload(self) -> None:
|
||||
@ -57,7 +58,6 @@ class WebServer:
|
||||
|
||||
def setup(self) -> None:
|
||||
self._setup_flask_app()
|
||||
self._setup_flask_headers()
|
||||
self._setup_web_globals()
|
||||
self._setup_web_errors()
|
||||
self._setup_web_controllers()
|
||||
@ -160,19 +160,6 @@ class WebServer:
|
||||
def inject_global_vars() -> dict:
|
||||
return self._template_renderer.get_view_globals()
|
||||
|
||||
def _setup_flask_headers(self) -> None:
|
||||
@self._app.after_request
|
||||
def modify_headers(response):
|
||||
# Supprimer les en-têtes de sécurité
|
||||
response.headers.pop('X-Frame-Options', None)
|
||||
response.headers.pop('X-Content-Type-Options', None)
|
||||
response.headers.pop('X-XSS-Protection', None)
|
||||
|
||||
# Modifier ou supprimer la politique CSP
|
||||
response.headers['Content-Security-Policy'] = "frame-ancestors *"
|
||||
|
||||
return response
|
||||
|
||||
def _setup_web_errors(self) -> None:
|
||||
def handle_error(error):
|
||||
if request.headers.get('Content-Type') == 'application/json' or request.headers.get('Accept') == 'application/json':
|
||||
@ -184,16 +171,14 @@ class WebServer:
|
||||
})
|
||||
return make_response(response, error.code)
|
||||
|
||||
return self._template_renderer.render_view(
|
||||
'@core/http-error.html',
|
||||
error_code=error.code,
|
||||
error_message=error.description
|
||||
)
|
||||
if error.code == 404:
|
||||
return send_from_directory(self.get_template_dir(), 'core/error404.html'), 404
|
||||
|
||||
return error
|
||||
|
||||
self._app.register_error_handler(400, handle_error)
|
||||
self._app.register_error_handler(404, handle_error)
|
||||
self._app.register_error_handler(409, handle_error)
|
||||
self._app.register_error_handler(413, handle_error)
|
||||
self._app.register_error_handler(HttpClientException, handle_error)
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
try:
|
||||
import psutil
|
||||
except:
|
||||
pass
|
||||
|
||||
import os
|
||||
import psutil
|
||||
import platform
|
||||
import logging
|
||||
import os
|
||||
|
||||
@ -1,133 +0,0 @@
|
||||
import struct
|
||||
import imghdr
|
||||
import os
|
||||
|
||||
|
||||
def get_png_size(file):
|
||||
file.seek(16)
|
||||
width, height = struct.unpack('>ii', file.read(8))
|
||||
return width, height
|
||||
|
||||
|
||||
def get_jpeg_size(file):
|
||||
file.seek(0)
|
||||
size = 2
|
||||
ftype = 0
|
||||
while not 0xC0 <= ftype <= 0xCF:
|
||||
file.seek(size, 1)
|
||||
byte = file.read(1)
|
||||
while ord(byte) == 0xFF:
|
||||
byte = file.read(1)
|
||||
ftype = ord(byte)
|
||||
size = struct.unpack('>H', file.read(2))[0] - 2
|
||||
file.seek(1, 1)
|
||||
height, width = struct.unpack('>HH', file.read(4))
|
||||
return width, height
|
||||
|
||||
|
||||
def get_gif_size(file):
|
||||
file.seek(6)
|
||||
width, height = struct.unpack('<HH', file.read(4))
|
||||
return width, height
|
||||
|
||||
|
||||
def get_webp_size(file):
|
||||
file.seek(12)
|
||||
chunk_header = file.read(8)
|
||||
while chunk_header:
|
||||
chunk_size = struct.unpack('<I', chunk_header[4:])[0]
|
||||
if chunk_header[0:4] == b'VP8 ':
|
||||
vp8_header = file.read(10)
|
||||
width, height = struct.unpack('<HH', vp8_header[6:10])
|
||||
return width, height
|
||||
elif chunk_header[0:4] == b'VP8L':
|
||||
vp8l_header = file.read(5)
|
||||
b1, b2, b3, b4 = struct.unpack('<BBBB', vp8l_header[1:])
|
||||
width = (b1 | ((b2 & 0x3F) << 8)) + 1
|
||||
height = ((b2 >> 6) | (b3 << 2) | ((b4 & 0x0F) << 10)) + 1
|
||||
return width, height
|
||||
elif chunk_header[0:4] == b'VP8X':
|
||||
vp8x_header = file.read(10)
|
||||
width, height = struct.unpack('<HH', vp8x_header[4:8])
|
||||
width = (width + 1) & 0xFFFFFF
|
||||
height = (height + 1) & 0xFFFFFF
|
||||
return width, height
|
||||
else:
|
||||
file.seek(chunk_size, 1)
|
||||
chunk_header = file.read(8)
|
||||
raise ValueError("Not a valid WebP file")
|
||||
|
||||
|
||||
def get_bmp_size(file):
|
||||
file.seek(18)
|
||||
width, height = struct.unpack('<ii', file.read(8))
|
||||
return width, height
|
||||
|
||||
|
||||
def get_tiff_size(file):
|
||||
file.seek(0)
|
||||
byte_order = file.read(2)
|
||||
if byte_order == b'II':
|
||||
endian = '<'
|
||||
elif byte_order == b'MM':
|
||||
endian = '>'
|
||||
else:
|
||||
raise ValueError("Not a valid TIFF file")
|
||||
|
||||
file.seek(4)
|
||||
offset = struct.unpack(endian + 'I', file.read(4))[0]
|
||||
file.seek(offset)
|
||||
|
||||
while True:
|
||||
num_tags = struct.unpack(endian + 'H', file.read(2))[0]
|
||||
for _ in range(num_tags):
|
||||
tag = file.read(12)
|
||||
if struct.unpack(endian + 'H', tag[:2])[0] == 256:
|
||||
width = struct.unpack(endian + 'I', tag[8:])[0]
|
||||
elif struct.unpack(endian + 'H', tag[:2])[0] == 257:
|
||||
height = struct.unpack(endian + 'I', tag[8:])[0]
|
||||
next_ifd_offset = struct.unpack(endian + 'I', file.read(4))[0]
|
||||
if next_ifd_offset == 0:
|
||||
break
|
||||
file.seek(next_ifd_offset)
|
||||
return width, height
|
||||
|
||||
|
||||
def get_ico_size(file):
|
||||
file.seek(6)
|
||||
num_images = struct.unpack('<H', file.read(2))[0]
|
||||
largest_size = (0, 0)
|
||||
for _ in range(num_images):
|
||||
width, height = struct.unpack('<BB', file.read(2))
|
||||
width = width if width != 0 else 256
|
||||
height = height if height != 0 else 256
|
||||
if width * height > largest_size[0] * largest_size[1]:
|
||||
largest_size = (width, height)
|
||||
file.seek(14, 1) # Skip over the rest of the directory entry
|
||||
return largest_size
|
||||
|
||||
|
||||
def get_picture_metadata(image_path):
|
||||
# Determine the image type using imghdr and file extension as a fallback
|
||||
img_type = imghdr.what(image_path)
|
||||
if not img_type:
|
||||
_, ext = os.path.splitext(image_path)
|
||||
img_type = ext.lower().replace('.', '')
|
||||
|
||||
with open(image_path, 'rb') as file:
|
||||
if img_type == 'png':
|
||||
return get_png_size(file)
|
||||
elif img_type == 'jpeg' or img_type == 'jpg':
|
||||
return get_jpeg_size(file)
|
||||
elif img_type == 'gif':
|
||||
return get_gif_size(file)
|
||||
elif img_type == 'webp':
|
||||
return get_webp_size(file)
|
||||
elif img_type == 'bmp':
|
||||
return get_bmp_size(file)
|
||||
elif img_type == 'tiff' or img_type == 'tif':
|
||||
return get_tiff_size(file)
|
||||
elif img_type == 'ico':
|
||||
return get_ico_size(file)
|
||||
else:
|
||||
raise ValueError("Unsupported image format or corrupted file")
|
||||
@ -1,40 +1,18 @@
|
||||
import struct
|
||||
import logging
|
||||
import subprocess
|
||||
import json
|
||||
from pymediainfo import MediaInfo
|
||||
|
||||
|
||||
def get_video_metadata(filename):
|
||||
try:
|
||||
result = subprocess.check_output(f'ffprobe -v quiet -show_streams -select_streams v:0 -of json "{filename}"', shell=True).decode()
|
||||
fields = json.loads(result)['streams'][0]
|
||||
duration = 0
|
||||
def mp4_duration_with_ffprobe(filename):
|
||||
import subprocess, json
|
||||
|
||||
if 'tags' in fields and 'DURATION' in fields['tags']:
|
||||
duration = round(float(fields['tags']['DURATION']), 2)
|
||||
elif 'duration' in fields:
|
||||
duration = round(float(fields['duration']), 2)
|
||||
result = subprocess.check_output(
|
||||
f'ffprobe -v quiet -show_streams -select_streams v:0 -of json "{filename}"',
|
||||
shell=True).decode()
|
||||
fields = json.loads(result)['streams'][0]
|
||||
|
||||
width = fields.get('width', 0)
|
||||
height = fields.get('height', 0)
|
||||
if 'tags' in fields and 'DURATION' in fields['tags']:
|
||||
return round(float(fields['tags']['DURATION']), 2)
|
||||
|
||||
return width, height, duration
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
logging.warn("ffprobe not found or an error occurred. Using pymediainfo instead.")
|
||||
if 'duration' in fields:
|
||||
return round(float(fields['duration']), 2)
|
||||
|
||||
try:
|
||||
media_info = MediaInfo.parse(filename)
|
||||
for track in media_info.tracks:
|
||||
if track.track_type == "Video":
|
||||
duration = round(track.duration / 1000, 2) if track.duration else None
|
||||
width = track.width
|
||||
height = track.height
|
||||
|
||||
return width, height, duration
|
||||
except OSError:
|
||||
logging.warn("Fail to get video metadata from pymediainfo.")
|
||||
except json.JSONDecodeError:
|
||||
logging.warn("Fail to get video metadata from ffprobe.")
|
||||
|
||||
return 0, 0, 0
|
||||
return 0
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
STUDIO_URL=http://localhost:5000 # Main Obscreen Studio instance URL (could be a specific playlist /use/[playlist-id] or let obscreen manage playlist routing with /)
|
||||
TARGET_RESOLUTION=auto # e.g. 1920x1080 - Force specific resolution (supported list available with command `DISPLAY=:0 xrandr`)
|
||||
|
||||
# Disable screensaver and DPMS
|
||||
xset s off
|
||||
xset -dpms
|
||||
@ -13,15 +7,10 @@ xset s noblank
|
||||
unclutter -display :0 -noevents -grab &
|
||||
|
||||
# Modify Chromium preferences to avoid restore messages
|
||||
mkdir -p /tmp/obscreen/chromium/Default 2>/dev/null
|
||||
touch /tmp/obscreen/chromium/Default/Preferences
|
||||
sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' /tmp/obscreen/chromium/Default/Preferences
|
||||
mkdir -p /home/pi/.config/chromium/Default 2>/dev/null
|
||||
touch /home/pi/.config/chromium/Default/Preferences
|
||||
sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' /home/pi/.config/chromium/Default/Preferences
|
||||
|
||||
# Resolution setup
|
||||
if [ "$TARGET_RESOLUTION" != "auto" ]; then
|
||||
FIRST_CONNECTED_SCREEN=$(xrandr | grep " connected" | awk '{print $1}' | head -n 1)
|
||||
xrandr --output $FIRST_CONNECTED_SCREEN --mode $TARGET_RESOLUTION
|
||||
fi
|
||||
RESOLUTION=$(DISPLAY=:0 xrandr | grep '*' | awk '{print $1}')
|
||||
WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1)
|
||||
HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2)
|
||||
@ -40,9 +29,7 @@ chromium-browser \
|
||||
--noerrdialogs \
|
||||
--kiosk \
|
||||
--incognito \
|
||||
--user-data-dir=/tmp/obscreen/chromium \
|
||||
--no-sandbox \
|
||||
--window-position=0,0 \
|
||||
--window-size=${WIDTH},${HEIGHT} \
|
||||
--display=:0 \
|
||||
${STUDIO_URL}
|
||||
http://localhost:5000
|
||||
|
||||
@ -52,40 +52,14 @@ fi
|
||||
# Installation
|
||||
# ============================================================
|
||||
|
||||
|
||||
echo ""
|
||||
echo "# Waiting 3 seconds before installation..."
|
||||
sleep 3
|
||||
|
||||
# Update and install necessary packages
|
||||
apt update
|
||||
|
||||
# ------------------
|
||||
# Chromium package
|
||||
# ------------------
|
||||
CHROMIUM=""
|
||||
|
||||
# Attempt to install chromium-browser
|
||||
if sudo apt-get install -y chromium-browser; then
|
||||
CHROMIUM="chromium-browser"
|
||||
else
|
||||
if sudo apt-get install -y chromium; then
|
||||
CHROMIUM="chromium"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$CHROMIUM" ]; then
|
||||
echo "Error: Chromium could not be installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ------------------
|
||||
# Remaining packages
|
||||
# ------------------
|
||||
apt install -y xinit xserver-xorg x11-xserver-utils unclutter pulseaudio
|
||||
|
||||
# ------------------
|
||||
# Configuration
|
||||
# ------------------
|
||||
apt install -y xinit xserver-xorg chromium-browser unclutter pulseaudio
|
||||
|
||||
# Add user to tty, video groups
|
||||
usermod -aG tty,video $OWNER
|
||||
@ -96,7 +70,7 @@ grep -qxF "allowed_users=anybody" /etc/X11/Xwrapper.config || echo "allowed_user
|
||||
grep -qxF "needs_root_rights=yes" /etc/X11/Xwrapper.config || echo "needs_root_rights=yes" | tee -a /etc/X11/Xwrapper.config
|
||||
|
||||
# Create the systemd service to start Chromium in kiosk mode
|
||||
curl https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/system/obscreen-player.service | sed "s#/home/pi#$WORKING_DIR#g" | sed "s#=pi#=$OWNER#g" | tee /etc/systemd/system/obscreen-player.service
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/system/obscreen-player.service | sed "s#/home/pi#$WORKING_DIR#g" | sed "s#=pi#=$OWNER#g" | tee /etc/systemd/system/obscreen-player.service
|
||||
|
||||
# Reload systemd, enable and start the service
|
||||
systemctl daemon-reload
|
||||
@ -108,7 +82,7 @@ systemctl set-default graphical.target
|
||||
# ============================================================
|
||||
|
||||
mkdir -p "$WORKING_DIR/obscreen/var/run"
|
||||
curl https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/system/autostart-browser-x11.sh | sed "s#/home/pi#$WORKING_DIR#g" | sed "s#=pi#=$OWNER#g" | sed "s#chromium-browser#$CHROMIUM#g" | sed "s#http://localhost:5000#$obscreen_studio_url#g" | tee "$WORKING_DIR/obscreen/var/run/play"
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/system/autostart-browser-x11.sh | sed "s#/home/pi#$WORKING_DIR#g" | sed "s#=pi#=$OWNER#g" | sed "s#http://localhost:5000#$obscreen_studio_url#g" | tee "$WORKING_DIR/obscreen/var/run/play"
|
||||
chmod +x "$WORKING_DIR/obscreen/var/run/play"
|
||||
chown -R $OWNER:$OWNER "$WORKING_DIR/obscreen"
|
||||
|
||||
|
||||
@ -19,11 +19,11 @@ sleep 3
|
||||
|
||||
# Install system dependencies
|
||||
apt-get update
|
||||
apt-get install -y git build-essential gcc python3-dev python3-pip python3-venv libsqlite3-dev ntfs-3g ffmpeg
|
||||
apt-get install -y git python3-pip python3-venv libsqlite3-dev ntfs-3g ffmpeg
|
||||
|
||||
# Get files
|
||||
cd $WORKING_DIR
|
||||
git clone https://github.com/csmith1865/obscreen.git
|
||||
git clone https://github.com/jr-k/obscreen.git
|
||||
cd obscreen
|
||||
|
||||
# Install application dependencies
|
||||
@ -44,7 +44,7 @@ chown -R $OWNER:$OWNER ./
|
||||
# Automount script for external storage
|
||||
# ============================================================
|
||||
|
||||
curl https://raw.githubusercontent.com/csmith1865/obscreen/master/system/external-storage/10-obscreen-media-automount.rules | sed "s#/home/pi#$WORKING_DIR#g" | tee /etc/udev/rules.d/10-obscreen-media-automount.rules
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/system/external-storage/10-obscreen-media-automount.rules | sed "s#/home/pi#$WORKING_DIR#g" | tee /etc/udev/rules.d/10-obscreen-media-automount.rules
|
||||
udevadm control --reload-rules
|
||||
systemctl restart udev
|
||||
udevadm trigger
|
||||
|
||||
@ -1 +1 @@
|
||||
2.4.4
|
||||
2.4.2
|
||||
@ -15,7 +15,8 @@
|
||||
|
||||
{% block body_class %}view-auth-user-list{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
|
||||
<div class="top-content">
|
||||
<div class="top-actions">
|
||||
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_START) }}
|
||||
@ -25,9 +26,12 @@
|
||||
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_END) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ l[error] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-content">
|
||||
|
||||
@ -14,7 +14,14 @@
|
||||
|
||||
{% block body_class %}view-login{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% block page %}
|
||||
|
||||
{% if login_error %}
|
||||
<div class="alert alert-error">
|
||||
{{ t('login_error_' ~ login_error) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="login-content">
|
||||
<div class="form-holder">
|
||||
<div class="card">
|
||||
|
||||
@ -180,14 +180,14 @@
|
||||
|
||||
<div class="context-divider"></div>
|
||||
|
||||
<div class="{% if not AUTH_ENABLED %}context-tail{% else %}context-tail-auth{% endif %}">
|
||||
<div class="{% if not AUTH_ENABLED %}contex-tail{% endif %}">
|
||||
<a href="{{ url_for('slideshow_player_refresh', next=request.full_path) }}" class="btn btn-naked btn-double-icon">
|
||||
<i class="fa fa-display main"></i>
|
||||
<sub><i class="fa fa-refresh"></i></sub>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if fully_authenticated_view and AUTH_ENABLED %}
|
||||
{% if fully_authenticated_view %}
|
||||
<div class="context-divider"></div>
|
||||
<div class="context-user">
|
||||
<div class="dropdown">
|
||||
@ -213,41 +213,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="main-container">
|
||||
{% block top_page %}{% endblock %}
|
||||
{% if request.args.get('refresh_player') %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fa fa-refresh icon-left"></i>
|
||||
{{ l.slideshow_slide_refresh_player_success|replace('%time%', request.args.get('refresh_player')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block toolbar_page %}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
{% set alert_class = '' %}
|
||||
{% set alert_icon = '' %}
|
||||
|
||||
{% if category == 'success' %}
|
||||
{% set alert_class = 'success' %}
|
||||
{% set alert_icon = 'fa-check' %}
|
||||
{% elif category == 'success:refresh' %}
|
||||
{% set alert_class = 'success' %}
|
||||
{% set alert_icon = 'fa-refresh' %}
|
||||
{% elif category == 'error' %}
|
||||
{% set alert_class = 'danger' %}
|
||||
{% set alert_icon = 'fa-warning' %}
|
||||
{% elif category == 'warning' %}
|
||||
{% set alert_class = 'yellow' %}
|
||||
{% set alert_icon = 'fa-warning' %}
|
||||
{% endif %}
|
||||
|
||||
{% if alert_class %}
|
||||
<div class="alert alert-{{ alert_class }} alert-timeout">
|
||||
<i class="fa {{ alert_icon }} icon-left"></i>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}{% endblock %}
|
||||
{% block page %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -256,7 +229,6 @@
|
||||
<script>
|
||||
var secret_key = '{{ SECRET_KEY }}';
|
||||
var l = {
|
||||
'js_common_empty': '{{ l.common_empty }}',
|
||||
'js_common_are_you_sure': '{{ l.common_are_you_sure }}',
|
||||
'js_playlist_delete_confirmation': '{{ l.js_playlist_delete_confirmation }}',
|
||||
'js_slideshow_slide_delete_confirmation': '{{ l.js_slideshow_slide_delete_confirmation }}',
|
||||
@ -271,7 +243,6 @@
|
||||
};
|
||||
</script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery.min.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-more.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/utils.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/global.js"></script>
|
||||
{{ HOOK(H_ROOT_JAVASCRIPT) }}
|
||||
|
||||
@ -10,7 +10,8 @@
|
||||
|
||||
{% block body_class %}view-logs-list{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% block page %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-content">
|
||||
<div class="inner">
|
||||
|
||||
@ -11,7 +11,20 @@
|
||||
|
||||
{% block body_class %}view-plugins-list{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% block page %}
|
||||
|
||||
{% if request.args.get('error') %}
|
||||
<div class="alert alert-error">
|
||||
{{ t(request.args.get('error')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.args.get('info') %}
|
||||
<div class="alert alert-info">
|
||||
{{ t(request.args.get('info')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-content">
|
||||
<div class="inner">
|
||||
|
||||
@ -11,7 +11,20 @@
|
||||
|
||||
{% block body_class %}view-settings-list{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% block page %}
|
||||
|
||||
{% if request.args.get('error') %}
|
||||
<div class="alert alert-error">
|
||||
{{ t(request.args.get('error')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.args.get('warning') %}
|
||||
<div class="alert alert-yellow">
|
||||
<i class="fa fa-warning"></i>{{ t(request.args.get('warning')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-content">
|
||||
<div class="inner">
|
||||
@ -29,4 +42,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
{% block body_class %}view-sysinfo-list{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
<div class="top-content">
|
||||
<div class="top-actions align-right">
|
||||
{{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_START) }}
|
||||
@ -24,9 +24,7 @@
|
||||
{{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_END) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
<div class="bottom-content">
|
||||
<div class="page-content">
|
||||
<div class="inner">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -6,10 +6,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/flatpickr.min.css"/>
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/jquery-explr-1.4.css"/>
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/fleet/node-players.js"></script>
|
||||
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
||||
@ -17,15 +20,20 @@
|
||||
|
||||
{% block body_class %}view-node-player-edit edit-page{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
<div class="top-content">
|
||||
<h1>
|
||||
{{ l.fleet_node_player_form_edit_title }}
|
||||
</h1>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% if request.args.get('saved') %}
|
||||
<div class="alert alert-success alert-timeout">
|
||||
<i class="fa fa-check icon-left"></i>
|
||||
{{ l.common_saved }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-content">
|
||||
<div class="inner dirview">
|
||||
|
||||
@ -20,7 +20,9 @@
|
||||
|
||||
{% block body_class %}view-node-player-list{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
{% set explr_limit_chars = 35 %}
|
||||
|
||||
<div class="top-content">
|
||||
<div class="top-actions">
|
||||
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_START) }}
|
||||
@ -59,10 +61,20 @@
|
||||
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_END) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% set explr_limit_chars = 35 %}
|
||||
{% if request.args.get('folder_not_empty_error') %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa fa-warning icon-left"></i>
|
||||
{{ l.common_folder_not_empty_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.args.get('referenced_in_node_player_group_error') %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa fa-warning icon-left"></i>
|
||||
{{ l.fleet_node_player_referenced_in_node_player_group_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-panel left-panel explr-explorer">
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
<h2>
|
||||
{{ l.common_pick_element }}
|
||||
</h2>
|
||||
|
||||
{% with use_href=False %}
|
||||
{% include 'fleet/node-players/component/explr-sidebar.jinja.html' %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn btn-naked picker-close">
|
||||
<i class="fa fa-close icon-left"></i>{{ l.common_close }}
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
{% block body_class %}view-player-group-list{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
<div class="top-content">
|
||||
<div class="top-actions">
|
||||
{{ HOOK(H_FLEET_NODE_PLAYER_GROUP_TOOLBAR_ACTIONS_START) }}
|
||||
@ -46,9 +46,13 @@
|
||||
('<a href="javascript:void(0);" class="item-add node-player-group-add">'~l.fleet_node_player_group_button_add~'</a>')|safe
|
||||
) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ l[error] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-panel left-panel">
|
||||
{% with node_player_groups=node_player_groups %}
|
||||
|
||||
@ -1,125 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% set preview_mode = request.args.get('preview') == '1' %}
|
||||
<style>
|
||||
html, body, #screen {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: black;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
iframe {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
{% if preview_mode %}
|
||||
html, body, #screen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
#screen {
|
||||
width: 1280px;
|
||||
height: 720px;
|
||||
outline: 5px solid white;
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="screen"></div>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery.min.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/utils.js"></script>
|
||||
<script>
|
||||
const contentData = JSON.parse({{ json_dumps(content.location) | safe }});
|
||||
const baseIframeRoute = '{{ url_for('player', preview_content_id='!c!', autoplay=1, cover=1, transparent=1) }}';
|
||||
|
||||
jQuery(function($) {
|
||||
function setOptimalSize() {
|
||||
const ratio = evalStringRatio(contentData.ratio);
|
||||
const bodyWidth = $('body').width() - 100;
|
||||
const bodyHeight = $('body').height() - 100;
|
||||
|
||||
let width = bodyWidth;
|
||||
let height = bodyWidth / ratio;
|
||||
|
||||
if (height > bodyHeight) {
|
||||
height = bodyHeight;
|
||||
width = bodyHeight * ratio;
|
||||
}
|
||||
|
||||
const screenSizes = {
|
||||
width: Math.floor(width),
|
||||
height: Math.floor(height)
|
||||
};
|
||||
|
||||
$('#screen').css({
|
||||
'width': screenSizes.width,
|
||||
'height': screenSizes.height,
|
||||
});
|
||||
}
|
||||
|
||||
function createElement(config = null) {
|
||||
const screen = $('#screen');
|
||||
const offsetX = screen.position().left;
|
||||
const offsetY = screen.position().top;
|
||||
const screenWidth = screen.width();
|
||||
const screenHeight = screen.height();
|
||||
|
||||
const elementWidth = (config.widthPercent / 100) * screenWidth
|
||||
const elementHeight = (config.heightPercent / 100) * screenHeight;
|
||||
let x = offsetX + (config.xPercent / 100) * screenWidth;
|
||||
let y = offsetY + (config.yPercent / 100) * screenHeight;
|
||||
const zIndex = config.zIndex;
|
||||
|
||||
//x = Math.round(Math.max(0, Math.min(x, screenWidth - elementWidth)));
|
||||
//y = Math.round(Math.max(0, Math.min(y, screenHeight - elementHeight)));
|
||||
|
||||
const element = $('<iframe class="element" id="element-' + zIndex + '" data-id="' + zIndex + '" src="'+baseIframeRoute.replace('!c!', config.contentId)+'" frameborder="0" allowtransparency="1"></iframe>');
|
||||
|
||||
element.css({
|
||||
left: x,
|
||||
top: y,
|
||||
width: elementWidth,
|
||||
height: elementHeight,
|
||||
zIndex: zIndex,
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
transform: `rotate(0deg)`
|
||||
});
|
||||
|
||||
screen.append(element);
|
||||
}
|
||||
|
||||
const applyElementsFromContent = function() {
|
||||
$('#screen').html('');
|
||||
|
||||
for (let i = 0; i < contentData.layers.length; i++) {
|
||||
if (contentData.layers[i].contentId !== null) {
|
||||
createElement(contentData.layers[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const main = function() {
|
||||
{% if preview_mode %}
|
||||
setOptimalSize();
|
||||
{% endif %}
|
||||
|
||||
applyElementsFromContent();
|
||||
};
|
||||
|
||||
$(window).on('resize', function() {
|
||||
main();
|
||||
});
|
||||
|
||||
main();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -3,8 +3,6 @@
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
const time_with_seconds = {{ 'true' if time_with_seconds.as_bool() else 'false' }};
|
||||
const last_hard_refresh_request = {{ hard_refresh_request }};
|
||||
const no_playlist = {% if noplaylist %}true{% else %}false{% endif %};
|
||||
let external_url = '{{ external_url.strip() }}';
|
||||
|
||||
function updateTime() {
|
||||
@ -37,15 +35,7 @@
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
const json_response = JSON.parse(xhr.responseText);
|
||||
external_url = json_response.external_url;
|
||||
setIps(json_response.interfaces);
|
||||
|
||||
if (no_playlist) {
|
||||
setTimeout(function () {
|
||||
if (last_hard_refresh_request != json_response.hard_refresh_request) {
|
||||
document.location.href = '{{ url_for('player') }}';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
setIps(json_response.interfaces)
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
|
||||
@ -5,37 +5,17 @@
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="google" content="notranslate">
|
||||
<link rel="shortcut icon" href="{{ STATIC_PREFIX }}/favicon.ico">
|
||||
{% set force_cover = request.args.get('cover') == '1' %}
|
||||
{% set transparent = request.args.get('transparent') == '1' %}
|
||||
{% if slide_animation_enabled %}
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/animate.min.css" />
|
||||
{% endif %}
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: {{ 'transparent' if transparent else 'white' }}; display: flex; flex-direction: row; justify-content: center; align-items: center; }
|
||||
.slide { display: flex; flex-direction: row; justify-content: center; align-items: center; background: {{ 'transparent' if transparent else 'black' }}; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: white; display: flex; flex-direction: row; justify-content: center; align-items: center; }
|
||||
.slide { display: flex; flex-direction: row; justify-content: center; align-items: center; background: black; }
|
||||
.slide, iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; padding-top: 0; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; }
|
||||
.slide iframe { background: white; }
|
||||
.slide img, .slide video { {% if force_cover %}width: 100%;{% endif %} height: 100%; }
|
||||
.slide .text-holder { width: 100%; height: 100%; display: flex; flex-direction: row; justify-content: center; align-items: center; box-sizing: border-box; }
|
||||
.slide .text {
|
||||
width: 100%; height: 100%; color: white; font-family: 'Arial', 'sans-serif'; font-size: 20px; display: flex; flex-direction: row; justify-content: center; align-items: center;
|
||||
word-break: break-all; flex: 1; align-self: stretch; text-align: center; max-width: 100%; box-sizing: border-box;
|
||||
}
|
||||
.slide img, .slide video { height: 100%; }
|
||||
.slide video { width: 100%; height: 100%; }
|
||||
@keyframes blink{50%{opacity:0;}}
|
||||
.cfx-blink{animation:1.5s linear infinite blinker;}
|
||||
.cfx-ffff-speed {animation-delay: 0.1s;}
|
||||
.cfx-fff-speed {animation-delay: 0.3s;}
|
||||
.cfx-ff-speed {animation-delay: 0.5s;}
|
||||
.cfx-f-speed {animation-delay: 0.8s;}
|
||||
.cfx-m-speed {animation-delay: 1s;}
|
||||
.cfx-s-speed {animation-delay: 1.3s;}
|
||||
.cfx-ss-speed {animation-delay: 1.5s;}
|
||||
.cfx-sss-speed {animation-delay: 1.8s;}
|
||||
.cfx-ssss-speed {animation-delay: 2s;}
|
||||
.cfx-sssss-speed {animation-delay: 3s;}
|
||||
</style>
|
||||
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/lib/jquery.min.js"></script>
|
||||
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/utils.js"></script>
|
||||
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/lib/is-cron-now.js"></script>
|
||||
</head>
|
||||
@ -62,12 +42,11 @@
|
||||
const playlistCheckResolutionMs = {{ polling_interval * 1000 }};
|
||||
|
||||
// Backend flag updates
|
||||
const lastHardRefreshRequest = items.hard_refresh_request;
|
||||
let needHardRefresh = null;
|
||||
|
||||
// Frontend config
|
||||
const syncWithTime = items['time_sync'];
|
||||
const previewMode = items['preview_mode'];
|
||||
const disableAutoplay = {{ 'false' if request.args.get('autoplay') == '1' else 'previewMode' }};
|
||||
const tickRefreshResolutionMs = 100;
|
||||
|
||||
// Frontend flag updates
|
||||
@ -128,7 +107,9 @@
|
||||
items = data;
|
||||
itemsLoadedProcess();
|
||||
|
||||
if (lastHardRefreshRequest != items.hard_refresh_request) {
|
||||
if (needHardRefresh === null) {
|
||||
needHardRefresh = items.hard_refresh_request;
|
||||
} else if (needHardRefresh != items.hard_refresh_request) {
|
||||
document.location.reload();
|
||||
}
|
||||
}).catch(function(err) {
|
||||
@ -360,18 +341,12 @@
|
||||
case 'picture':
|
||||
loadPicture(element, callbackReady, item);
|
||||
break;
|
||||
case 'text':
|
||||
loadText(element, callbackReady, item);
|
||||
break;
|
||||
case 'video':
|
||||
loadVideo(element, callbackReady, item);
|
||||
break;
|
||||
case 'youtube':
|
||||
loadYoutube(element, callbackReady, item);
|
||||
break;
|
||||
case 'composition':
|
||||
loadComposition(element, callbackReady, item);
|
||||
break;
|
||||
default:
|
||||
loadUrl(element, callbackReady, item);
|
||||
break;
|
||||
@ -388,65 +363,6 @@
|
||||
callbackReady(function() {});
|
||||
};
|
||||
|
||||
const loadText = function(element, callbackReady, item) {
|
||||
const contentData = JSON.parse(item.location);
|
||||
const $textHolder = $('<div class="text-holder">');
|
||||
const $text = $('<div class="text">');
|
||||
let insideText = contentData.textLabel;
|
||||
|
||||
if (contentData.scrollEnable) {
|
||||
const $wrapper = $('<marquee>');
|
||||
$wrapper.attr({ scrollamount: contentData.scrollSpeed, direction: contentData.scrollDirection, behavior: 'scroll', loop: -1 });
|
||||
$wrapper.append(insideText);
|
||||
insideText = $wrapper;
|
||||
}
|
||||
|
||||
$text.append(insideText);
|
||||
|
||||
let justifyContent = 'center';
|
||||
switch(contentData.textAlign) {
|
||||
case 'left': justifyContent = 'flex-start'; break;
|
||||
case 'right': justifyContent = 'flex-end'; break;
|
||||
}
|
||||
|
||||
$text.css({
|
||||
padding: contentData.margin + 'px',
|
||||
color: contentData.color,
|
||||
textAlign: contentData.textAlign,
|
||||
textDecoration: contentData.textUnderline ? 'underline' : 'normal',
|
||||
fontSize: contentData.fontSize + 'px',
|
||||
fontWeight: contentData.fontBold ? 'bold' : 'normal',
|
||||
fontStyle: contentData.fontItalic ? 'italic' : 'normal',
|
||||
fontFamily: contentData.fontFamily + ", 'Arial', 'sans-serif'",
|
||||
whiteSpace: contentData.singleLine ? 'nowrap' : 'normal',
|
||||
justifyContent: justifyContent
|
||||
});
|
||||
|
||||
$textHolder.css({ backgroundColor: contentData.backgroundColor }).html($text);
|
||||
element.innerHTML = $('<div>').html($textHolder).html();
|
||||
callbackReady(function() {});
|
||||
};
|
||||
|
||||
const loadComposition = function(element, callbackReady, item) {
|
||||
element.innerHTML = `composition`;
|
||||
callbackReady(function() {});
|
||||
|
||||
const loadingDelayMs = 1000;
|
||||
let delayNoisyContentJIT = Math.max(100, (lookupCurrentItem().duration * 1000) - loadingDelayMs);
|
||||
delayNoisyContentJIT = lookupCurrentItem().id !== item.id ? delayNoisyContentJIT : 0;
|
||||
|
||||
const autoplayLoader = function() {
|
||||
if (secondsBeforeNext * 1000 > loadingDelayMs) {
|
||||
return setTimeout(autoplayLoader, 500);
|
||||
}
|
||||
|
||||
if (element.innerHTML === 'composition') {
|
||||
element.innerHTML = `<iframe src="${item.location}" frameborder="0" allow="autoplay" allowfullscreen></iframe>`;
|
||||
}
|
||||
}
|
||||
setTimeout(autoplayLoader, delayNoisyContentJIT);
|
||||
};
|
||||
|
||||
const loadYoutube = function(element, callbackReady, item) {
|
||||
element.innerHTML = `youtube`;
|
||||
callbackReady(function() {});
|
||||
@ -461,7 +377,7 @@
|
||||
}
|
||||
|
||||
if (element.innerHTML === 'youtube') {
|
||||
const autoplay = disableAutoplay ? '0' : '1';
|
||||
const autoplay = previewMode ? '0' : '1';
|
||||
element.innerHTML = `<iframe src="https://www.youtube.com/embed/${item.location}?version=3&autoplay=${autoplay}&showinfo=0&controls=0&modestbranding=1&fs=1&rel=0" frameborder="0" allow="autoplay" allowfullscreen></iframe>`;
|
||||
}
|
||||
}
|
||||
@ -469,7 +385,7 @@
|
||||
};
|
||||
|
||||
const loadVideo = function(element, callbackReady, item) {
|
||||
element.innerHTML = `<video ${disableAutoplay ? 'controls' : ''}><source src=${item.location} type="video/mp4" /></video>`;
|
||||
element.innerHTML = `<video ${previewMode ? 'controls' : ''}><source src=${item.location} type="video/mp4" /></video>`;
|
||||
const video = element.querySelector('video');
|
||||
callbackReady(function() {});
|
||||
|
||||
@ -493,7 +409,7 @@
|
||||
}
|
||||
|
||||
if (element.innerHTML.match('<video>')) {
|
||||
if (!disableAutoplay) {
|
||||
if (!previewMode) {
|
||||
setTimeout(function() {
|
||||
video.play();
|
||||
pausableContent = video;
|
||||
|
||||
@ -54,18 +54,10 @@
|
||||
var script_sender = document.createElement('script');
|
||||
script_sender.src = "{{ STATIC_PREFIX }}js/lib/cast-sender.js";
|
||||
document.body.appendChild(script_sender);
|
||||
const isHttps = document.location.protocol.indexOf('https') === 0;
|
||||
const isLoopback = document.location.host.indexOf('localhost') === 0 || document.location.host.indexOf('127.0.0.1') === 0;
|
||||
|
||||
if (isLoopback || !isHttps) {
|
||||
var script_caster = document.createElement('script');
|
||||
script_caster.src = "{{ STATIC_PREFIX }}js/cast-url.js";
|
||||
document.body.appendChild(script_caster);
|
||||
} else {
|
||||
$(document).on('click', '.cast-url', function() {
|
||||
window.open('https://cast.obscreen.io/sender.html?url=' + encodeURIComponent($('#' + $(this).attr('data-target-id')).val()));
|
||||
});
|
||||
}
|
||||
var script_caster = document.createElement('script');
|
||||
script_caster.src = "{{ STATIC_PREFIX }}js/cast-url.js";
|
||||
document.body.appendChild(script_caster);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -82,7 +74,7 @@
|
||||
|
||||
{% block body_class %}view-playlist-list{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
<div class="top-content">
|
||||
<div class="top-actions">
|
||||
{{ HOOK(H_PLAYLIST_TOOLBAR_ACTIONS_START) }}
|
||||
@ -119,9 +111,13 @@
|
||||
('<a href="javascript:void(0);" class="item-add playlist-add">'~l.playlist_button_add~'</a>')|safe
|
||||
) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ l[error] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-panel left-panel">
|
||||
{% with playlists=playlists %}
|
||||
|
||||
@ -14,17 +14,17 @@
|
||||
{{ render_folder(child) }}
|
||||
{% endfor %}
|
||||
{% for content in content_children %}
|
||||
{% set slides = slides_with_content[content.id]|default([]) if slides_with_content else [] %}
|
||||
{% set slides = slides_with_content[content.id]|default([]) %}
|
||||
{% set icon = enum_content_type.get_fa_icon(content.type) %}
|
||||
{% set color = enum_content_type.get_color_icon(content.type) %}
|
||||
<li class="explr-item" data-entity-json="{{ content.to_json({'classIcon': icon, 'classColor': color, 'metadata': json_loads(content.metadata)}) }}">
|
||||
<li class="explr-item" data-entity-json="{{ content.to_json() }}">
|
||||
<i class="fa {{ icon }} {{ color }}"></i>
|
||||
{% if slides|length > 0 %}
|
||||
<sub>
|
||||
<i class="fa fa-play"></i>
|
||||
</sub>
|
||||
{% endif %}
|
||||
<a href="{% if use_href %}{{ url_for('slideshow_content_edit', content_id=content.id, path=folder.path) }}{% else %}javascript:void(0);{% endif %}"
|
||||
<a href="{% if use_href %}{{ url_for('slideshow_content_edit', content_id=content.id) }}{% else %}javascript:void(0);{% endif %}"
|
||||
class="{{ 'explr-pick-element' if not use_href }}">
|
||||
{{ content.name }}
|
||||
</a>
|
||||
|
||||
@ -1,248 +0,0 @@
|
||||
{% set active_pill_route='slideshow_content_list' %}
|
||||
{% extends 'base.jinja.html' %}
|
||||
|
||||
{% block page_title %}
|
||||
{{ l.slideshow_content_page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/jquery-explr-1.4.css"/>
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script>
|
||||
const content_type_icon_classes = {
|
||||
{% for type in enum_content_type %}
|
||||
'{{ type.value }}': '{{ enum_content_type.get_fa_icon(type) }}',
|
||||
{% endfor %}
|
||||
};
|
||||
const content_type_color_classes = {
|
||||
{% for type in enum_content_type %}
|
||||
'{{ type.value }}': '{{ enum_content_type.get_color_icon(type) }}',
|
||||
{% endfor %}
|
||||
};
|
||||
</script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/slideshow/contents.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
|
||||
{# <script src="{{ STATIC_PREFIX }} js/lib/jquery-ui-rotatable.min.js"></script> #}
|
||||
<script src="{{ STATIC_PREFIX }}js/slideshow/content-composition.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/explorer.js"></script>
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}view-content-edit view-content-edit-composition edit-page{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
<div class="top-content">
|
||||
<h1>
|
||||
{{ l.slideshow_content_form_edit_title }}
|
||||
</h1>
|
||||
|
||||
{% set icon = enum_content_type.get_fa_icon(content.type) %}
|
||||
{% set color = enum_content_type.get_color_icon(content.type) %}
|
||||
|
||||
<h3>
|
||||
<span class="{{ color }} border-{{ color }}">
|
||||
<i class="fa {{ icon }} {{ color }}"></i> {{ t(content.type) }}
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<a href="{{ url_for('serve_content_composition', content_id=content.id, autoplay=1, preview=1) }}" target="_blank" class="btn btn-naked">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
<div class="bottom-content">
|
||||
<div class="page-panel left-panel">
|
||||
<div class="inner dirview">
|
||||
<div class="breadcrumb-container">
|
||||
<ul class="breadcrumb">
|
||||
{% set ns = namespace(breadpath='') %}
|
||||
{% for dir in working_folder_path[1:].split('/') %}
|
||||
{% set ns.breadpath = ns.breadpath ~ '/' ~ dir %}
|
||||
<li>
|
||||
<a href="{{ url_for('slideshow_content_cd', path=ns.breadpath) }}">
|
||||
<i class="explr-icon explr-icon-folder"></i>
|
||||
{{ truncate(dir, 25, '...') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if not loop.last %}
|
||||
<li class="divider">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% set contentData = json_loads(content.location) %}
|
||||
<div class="horizontal">
|
||||
<div class="form-holder">
|
||||
<form class="form"
|
||||
action="{{ url_for('slideshow_content_save', content_id=content.id) }}?path={{ working_folder_path }}"
|
||||
method="POST">
|
||||
<div class="form-group">
|
||||
<label for="content-edit-name">{{ l.slideshow_content_form_label_name }}</label>
|
||||
<div class="widget">
|
||||
<input type="text" name="name" id="content-edit-name" required="required"
|
||||
value="{{ content.name }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="elem-screen-ratio">{{ l.enum_content_type_composition_object_label }}</label>
|
||||
<div class="widget">
|
||||
{% set ratios = [
|
||||
"4/3",
|
||||
"16/9",
|
||||
"16/10",
|
||||
"3/4",
|
||||
"9/16",
|
||||
"10/16",
|
||||
] %}
|
||||
<select name="name" id="elem-screen-ratio" required="required" class="size-m">
|
||||
{% for ratio in ratios %}
|
||||
<option value="{{ ratio }}" {% if ratio == contentData.ratio %}selected="selected"{% endif %}>
|
||||
{{ ratio }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# <div class="form-group">#}
|
||||
{# <label for="">Ratio</label>#}
|
||||
{# <div class="horizontal">#}
|
||||
{# <div class="widget">#}
|
||||
{# <input type="text" value="16" />#}
|
||||
{# </div>#}
|
||||
{# <div>#}
|
||||
{# /#}
|
||||
{# </div>#}
|
||||
{# <div class="widget">#}
|
||||
{# <input type="text" value="9" />#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
|
||||
<input type="hidden" name="location" id="content-edit-location" value="{{ content.location }}" />
|
||||
|
||||
<div class="elements-holder">
|
||||
<h3 class="divide">{{ l.composition_elements_heading }}</h3>
|
||||
<div class="form-elements-list" id="elementList">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions actions-right">
|
||||
<button type="submit" class="btn btn-info">
|
||||
<i class="fa fa-save icon-left"></i>
|
||||
{{ l.common_save }}
|
||||
</button>
|
||||
<a href="{{ url_for('slideshow_content_list') }}" class="btn btn-naked">
|
||||
<i class="fa fa-rectangle-xmark icon-left"></i>
|
||||
{{ l.common_cancel }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="page-content">
|
||||
<div class="inner">
|
||||
<h3 class="main">
|
||||
{{ l.composition_monitor }} <span class="ratio-value badge-inset"></span>
|
||||
</h3>
|
||||
|
||||
<div class="toolbar">
|
||||
<button id="addElement" class="content-explr-picker"><i class="fa fa-plus icon-left"></i>{{ l.composition_element_add }}</button>
|
||||
<button id="removeAllElements" class="btn btn-danger"><i class="fa fa-trash icon-left"></i> {{ l.composition_elements_delete_all }}</button>
|
||||
</div>
|
||||
|
||||
<div class="presets">
|
||||
<h4 class="divide">
|
||||
{{ l.composition_presets }}:
|
||||
</h4>
|
||||
<button type="button" id="presetGrid2x2" class="btn btn-wire-neutral">{{ l.composition_presets_grid_2x2 }}</button>
|
||||
<button type="button" id="presetTvNews1x1" class="btn btn-wire-neutral">{{ l.composition_presets_tvnews_1x1 }}</button>
|
||||
</div>
|
||||
<div class="screen-holder">
|
||||
<div class="screen" id="screen">
|
||||
<!-- Elements will be dynamically added here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="page-panel right-panel">
|
||||
<div class="form-element-properties hidden">
|
||||
<form id="elementForm">
|
||||
<h3>
|
||||
{{ l.common_position }}
|
||||
</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-x">{{ l.composition_element_x_axis }}</label>
|
||||
<div class="widget">
|
||||
<input type="number" id="elem-x" name="elem-x">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-y">{{ l.composition_element_y_axis }}</label>
|
||||
<div class="widget">
|
||||
<input type="number" id="elem-y" name="elem-y">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="divide">
|
||||
{{ l.common_size }}
|
||||
</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-width">{{ l.common_width }}</label>
|
||||
<div class="widget">
|
||||
<input type="number" id="elem-width" name="elem-width">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-height">{{ l.common_height }}</label>
|
||||
<div class="widget">
|
||||
<input type="number" id="elem-height" name="elem-height">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="horizontal fx-end element-tool element-adjust-aspect-ratio-container hidden">
|
||||
<button type="button" class="btn btn-wire-neutral element-adjust-aspect-ratio">
|
||||
<i class="fa fa-solid fa-down-left-and-up-right-to-center icon-left"></i> {{ l.composition_element_match_content_aspect_ratio }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# <div class="form-group">#}
|
||||
{# <label for="elem-rotate">{{ l.common_angle }} (deg)</label>#}
|
||||
{# <div class="widget">#}
|
||||
{# <input type="number" id="elem-rotate" name="elem-rotate">#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pickers hidden">
|
||||
<div class="modals-outer">
|
||||
<div class="modals-inner">
|
||||
{% include 'slideshow/contents/modal/explr-picker.jinja.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -1,311 +0,0 @@
|
||||
{% set active_pill_route='slideshow_content_list' %}
|
||||
{% extends 'base.jinja.html' %}
|
||||
|
||||
{% block page_title %}
|
||||
{{ l.slideshow_content_page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jscolor.min.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/slideshow/contents.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/slideshow/content-text.js"></script>
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}view-content-edit view-content-edit-text edit-page{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
<div class="top-content">
|
||||
<h1>
|
||||
{{ l.slideshow_content_form_edit_title }}
|
||||
</h1>
|
||||
|
||||
{% set icon = enum_content_type.get_fa_icon(content.type) %}
|
||||
{% set color = enum_content_type.get_color_icon(content.type) %}
|
||||
|
||||
<h3>
|
||||
<span class="{{ color }} border-{{ color }}">
|
||||
<i class="fa {{ icon }} {{ color }}"></i> {{ t(content.type) }}
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<a href="{{ url_for('player', preview_content_id=content.id) }}" target="_blank" class="btn btn-naked">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
<div class="bottom-content">
|
||||
<div class="page-panel left-panel">
|
||||
<div class="inner dirview">
|
||||
<div class="breadcrumb-container">
|
||||
<ul class="breadcrumb">
|
||||
{% set ns = namespace(breadpath='') %}
|
||||
{% for dir in working_folder_path[1:].split('/') %}
|
||||
{% set ns.breadpath = ns.breadpath ~ '/' ~ dir %}
|
||||
<li>
|
||||
<a href="{{ url_for('slideshow_content_cd', path=ns.breadpath) }}">
|
||||
<i class="explr-icon explr-icon-folder"></i>
|
||||
{{ truncate(dir, 25, '...') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if not loop.last %}
|
||||
<li class="divider">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="horizontal">
|
||||
<div class="form-holder">
|
||||
<form class="form"
|
||||
action="{{ url_for('slideshow_content_save', content_id=content.id) }}?path={{ working_folder_path }}"
|
||||
method="POST">
|
||||
<div class="form-group">
|
||||
<label for="content-edit-name">{{ l.slideshow_content_form_label_name }}</label>
|
||||
<div class="widget">
|
||||
<input type="text" name="name" id="content-edit-name" required="required"
|
||||
value="{{ content.name }}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="location" id="content-edit-location"
|
||||
value="{{ content.location }}"/>
|
||||
|
||||
<div class="actions actions-right">
|
||||
<button type="submit" class="btn btn-info">
|
||||
<i class="fa fa-save icon-left"></i>
|
||||
{{ l.common_save }}
|
||||
</button>
|
||||
<a href="{{ url_for('slideshow_content_list') }}" class="btn btn-naked">
|
||||
<i class="fa fa-rectangle-xmark icon-left"></i>
|
||||
{{ l.common_cancel }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="page-content">
|
||||
<div class="inner">
|
||||
<h3 class="main">
|
||||
{{ l.composition_monitor }}
|
||||
</h3>
|
||||
<div class="screen-holder">
|
||||
<div class="screen" id="screen">
|
||||
<!-- Elements will be dynamically added here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="page-panel right-panel">
|
||||
<div class="form-element-properties">
|
||||
<form id="elementForm">
|
||||
<h3>
|
||||
Text
|
||||
</h3>
|
||||
|
||||
{% set contentStyles = json_loads(content.location) %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-text">Text Label</label>
|
||||
<div class="widget">
|
||||
<input type="text" id="elem-text" name="textLabel"
|
||||
value="{{ contentStyles.textLabel }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="divide">
|
||||
Style
|
||||
</h3>
|
||||
|
||||
<div class="horizontal">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-font-size">Font Size</label>
|
||||
<div class="widget widget-unit">
|
||||
<input type="text" id="elem-font-size" name="fontSize" maxlength="3"
|
||||
class="numeric-input chars-3" value="{{ contentStyles.fontSize }}">
|
||||
<span>pt</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-fg-color">Text Color</label>
|
||||
<div class="widget">
|
||||
<input type="text" id="elem-fg-color" name="color" class="color-picker"
|
||||
data-jscolor="{ value: '#{{ contentStyles.color }}', backgroundColor: '#333333', shadowColor: '#000000', width: 120, height: 120 }"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-font-family">Text Font Type</label>
|
||||
<div class="widget">
|
||||
{% set fonts = [
|
||||
{"value": "Arial", "name": "Arial"},
|
||||
{"value": "Arial Black", "name": "Arial Black"},
|
||||
{"value": "Verdana", "name": "Verdana"},
|
||||
{"value": "Trebuchet MS", "name": "Trebuchet MS"},
|
||||
{"value": "Georgia", "name": "Georgia"},
|
||||
{"value": "Times New Roman", "name": "Times New Roman"},
|
||||
{"value": "Courier New", "name": "Courier New"},
|
||||
{"value": "Comic Sans MS", "name": "Comic Sans MS"},
|
||||
{"value": "Impact", "name": "Impact"},
|
||||
{"value": "Tahoma", "name": "Tahoma"},
|
||||
{"value": "Gill Sans", "name": "Gill Sans"},
|
||||
{"value": "Helvetica", "name": "Helvetica"},
|
||||
{"value": "Optima", "name": "Optima"},
|
||||
{"value": "Garamond", "name": "Garamond"},
|
||||
{"value": "Baskerville", "name": "Baskerville"},
|
||||
{"value": "Copperplate", "name": "Copperplate"},
|
||||
{"value": "Futura", "name": "Futura"},
|
||||
{"value": "Monaco", "name": "Monaco"},
|
||||
{"value": "Andale Mono", "name": "Andale Mono"}
|
||||
] %}
|
||||
<select name="fontFamily" id="elem-font-family" class="size-m">
|
||||
{% for font in fonts %}
|
||||
<option value="{{ font.value }}" {% if font.value == contentStyles.fontFamily %}selected="selected"{% endif %}>
|
||||
{{ font.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="horizontal">
|
||||
<div class="form-group">
|
||||
<label for="elem-fg-color">Text Style</label>
|
||||
<div class="widget">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="elem-font-bold" name="fontBold" value="bold" {{ 'checked' if contentStyles.fontBold }}>
|
||||
<label for="elem-font-bold" class="btn btn-neutral">
|
||||
<i class="fa fa-bold"></i>
|
||||
</label>
|
||||
<input type="checkbox" id="elem-font-italic" name="fontItalic" value="italic" {{ 'checked' if contentStyles.fontItalic }}>
|
||||
<label for="elem-font-italic" class="btn btn-neutral">
|
||||
<i class="fa fa-italic"></i>
|
||||
</label>
|
||||
<input type="checkbox" id="elem-text-underline" name="textUnderline" value="underline" {{ 'checked' if contentStyles.textUnderline }}>
|
||||
<label for="elem-text-underline" class="btn btn-neutral">
|
||||
<i class="fa fa-underline"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="elem-fg-color">Text Alignment</label>
|
||||
<div class="widget">
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="elem-text-align-left" name="textAlign" value="left" {{ 'checked' if contentStyles.textAlign == 'left' }}>
|
||||
<label for="elem-text-align-left" class="btn btn-neutral">
|
||||
<i class="fa fa-align-left"></i>
|
||||
</label>
|
||||
<input type="radio" id="elem-text-align-center" name="textAlign" value="center" {{ 'checked' if contentStyles.textAlign == 'center' }}>
|
||||
<label for="elem-text-align-center" class="btn btn-neutral">
|
||||
<i class="fa fa-align-center"></i>
|
||||
</label>
|
||||
<input type="radio" id="elem-text-align-right" name="textAlign" value="right" {{ 'checked' if contentStyles.textAlign == 'right' }}>
|
||||
<label for="elem-text-align-right" class="btn btn-neutral">
|
||||
<i class="fa fa-align-right"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="divide">
|
||||
Background
|
||||
</h3>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-bg-color">Background Color</label>
|
||||
<div class="widget">
|
||||
<input type="text" id="elem-bg-color" name="backgroundColor" class="color-picker"
|
||||
data-jscolor="{ value: '#{{ contentStyles.backgroundColor }}', backgroundColor: '#333333', shadowColor: '#000000', width: 120, height: 120 }"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="divide">
|
||||
Scrolling Effect
|
||||
</h3>
|
||||
|
||||
<div class="horizontal">
|
||||
<div class="form-group">
|
||||
<label for="elem-scroll-enable">Enable</label>
|
||||
<div class="widget">
|
||||
<div class="toggle">
|
||||
<input type="checkbox" name="scrollEnable" id="elem-scroll-enable" {{ 'checked' if contentStyles.scrollEnable }} />
|
||||
<label for="elem-scroll-enable"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="elem-scroll-direction">Direction</label>
|
||||
<div class="widget">
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="elem-scroll-direction-left" name="scrollDirection" value="left" {{ 'checked' if contentStyles.scrollDirection == 'left' }}>
|
||||
<label for="elem-scroll-direction-left" class="btn btn-neutral">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
</label>
|
||||
<input type="radio" id="elem-scroll-direction-right" name="scrollDirection" value="right" {{ 'checked' if contentStyles.scrollDirection == 'right' }}>
|
||||
<label for="elem-scroll-direction-right" class="btn btn-neutral">
|
||||
<i class="fa fa-arrow-right"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="elem-scroll-speed">Speed</label>
|
||||
<div class="widget widget-unit">
|
||||
<input type="text" id="elem-scroll-speed" name="scrollSpeed" maxlength="3" class="numeric-input chars-3" value="{{ contentStyles.scrollSpeed }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h3 class="divide">
|
||||
Layout
|
||||
</h3>
|
||||
|
||||
<div class="horizontal">
|
||||
<div class="form-group">
|
||||
<label for="elem-single-line">Single Line Only</label>
|
||||
<div class="widget">
|
||||
<div class="toggle">
|
||||
<input type="checkbox" name="singleLine" id="elem-single-line" {{ 'checked' if contentStyles.singleLine }} />
|
||||
<label for="elem-single-line"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="elem-container-margin">Container Margin</label>
|
||||
<div class="widget widget-unit">
|
||||
<input type="text" id="elem-container-margin" name="margin" maxlength="3" class="numeric-input chars-3" value="{{ contentStyles.margin }}">
|
||||
<span>pt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@ -6,10 +6,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/flatpickr.min.css"/>
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/jquery-explr-1.4.css"/>
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/slideshow/contents.js"></script>
|
||||
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
||||
@ -17,24 +20,20 @@
|
||||
|
||||
{% block body_class %}view-content-edit edit-page{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
<div class="top-content">
|
||||
<h1>
|
||||
{{ l.slideshow_content_form_edit_title }}
|
||||
</h1>
|
||||
|
||||
{% set icon = enum_content_type.get_fa_icon(content.type) %}
|
||||
{% set color = enum_content_type.get_color_icon(content.type) %}
|
||||
|
||||
<h3>
|
||||
<span class="{{ color }} border-{{ color }}">
|
||||
<i class="fa {{ icon }} {{ color }}"></i> {{ t(content.type) }}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
{% if request.args.get('saved') %}
|
||||
<div class="alert alert-success alert-timeout">
|
||||
<i class="fa fa-check icon-left"></i>
|
||||
{{ l.common_saved }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-content">
|
||||
<div class="page-content">
|
||||
<div class="inner dirview">
|
||||
@ -81,7 +80,7 @@
|
||||
{% if content.type == enum_content_type.EXTERNAL_STORAGE %}
|
||||
<input type="text" class="disabled" disabled value="{{ external_storage_mountpoint }}/" />
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% set location = content.location %}
|
||||
{% if content.type == enum_content_type.YOUTUBE %}
|
||||
{% set location = 'https://www.youtube.com/watch?v=' ~ content.location %}
|
||||
@ -90,24 +89,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if content.type == enum_content_type.VIDEO or content.type == enum_content_type.PICTURE %}
|
||||
<div class="horizontal">
|
||||
<div class="form-group">
|
||||
<label for="">{{ l.common_width }}</label>
|
||||
<div class="widget">
|
||||
<input type="text" class="size-m" value="{{ content.get_metadata(enum_content_metadata.WIDTH) }}" disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="">{{ l.common_height }}</label>
|
||||
<div class="widget">
|
||||
<input type="text" class="size-m" value="{{ content.get_metadata(enum_content_metadata.HEIGHT) }}" disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="actions actions-right">
|
||||
<div class="actions actions-left">
|
||||
<button type="submit" class="btn btn-info">
|
||||
<i class="fa fa-save icon-left"></i>
|
||||
{{ l.common_save }}
|
||||
@ -125,6 +107,14 @@
|
||||
|
||||
|
||||
<div class="page-panel right-panel">
|
||||
{% set icon = enum_content_type.get_fa_icon(content.type) %}
|
||||
{% set color = enum_content_type.get_color_icon(content.type) %}
|
||||
|
||||
<h3>
|
||||
<span class="{{ color }} border-{{ color }}">
|
||||
<i class="fa {{ icon }} {{ color }}"></i> {{ t(content.type) }}
|
||||
</span>
|
||||
</h3>
|
||||
<div class="iframe-wrapper">
|
||||
<iframe src="{{ url_for('player', preview_content_id=content.id) }}"></iframe>
|
||||
</div>
|
||||
|
||||
@ -22,14 +22,16 @@
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-fileupload.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-multidraggable.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/super-upload.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/dragdrop.js"></script>
|
||||
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}view-content-list{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
{% set explr_limit_chars = 35 %}
|
||||
|
||||
<div class="top-content">
|
||||
<div class="top-actions">
|
||||
{{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }}
|
||||
@ -83,11 +85,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-upload alert-danger hidden"></div>
|
||||
{% endblock %}
|
||||
{% if request.args.get('folder_not_empty_error') %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa fa-warning icon-left"></i>
|
||||
{{ l.common_folder_not_empty_error }}
|
||||
</div>
|
||||
{% elif request.args.get('referenced_in_slide_error') %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa fa-warning icon-left"></i>
|
||||
{{ l.slideshow_content_referenced_in_slide_error }}
|
||||
</div>
|
||||
{% elif request.args.get('error') %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa fa-warning icon-left"></i>
|
||||
{{ t(request.args.get('error')) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-danger hidden"></div>
|
||||
{% endif %}
|
||||
|
||||
{% block main_page %}
|
||||
{% set explr_limit_chars = 35 %}
|
||||
<div class="bottom-content">
|
||||
<div class="page-panel left-panel explr-explorer">
|
||||
{% with use_href=True %}
|
||||
|
||||
@ -52,28 +52,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="from-group-condition hidden">
|
||||
<div class="form-group">
|
||||
<label for="" class="object-label"></label>
|
||||
<div class="widget">
|
||||
{% set ratios = [
|
||||
"16/9",
|
||||
"16/10",
|
||||
"4/3",
|
||||
"9/16",
|
||||
"10/16",
|
||||
"3/4",
|
||||
] %}
|
||||
<select name="object" data-input-type="composition" class="content-object-input size-m">
|
||||
{% for ratio in ratios %}
|
||||
<option value="{{ ratio }}">
|
||||
{{ ratio }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="from-group-condition hidden">
|
||||
<div class="form-group">
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
<button type="button" class="btn btn-naked content-explr-picker">
|
||||
<i class="fa fa-crosshairs"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-neutral hidden slide-content-show" data-route="{{ url_for('slideshow_content_edit', content_id='__id__') }}">
|
||||
<button type="button" class="btn btn-neutral hidden slide-content-show" data-route="{{ url_for('slideshow_content_show', content_id='__id__') }}">
|
||||
<i class="fa-solid fa-up-right-from-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -88,23 +88,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="horizontal">
|
||||
<div class="form-group slide-delegate-duration-group">
|
||||
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
|
||||
<div class="widget">
|
||||
<div class="toggle">
|
||||
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
|
||||
<label for="{{ tclass }}-delegate-duration"></label>
|
||||
</div>
|
||||
<div class="form-group form-group-horizontal slide-delegate-duration-group">
|
||||
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
|
||||
<div class="widget">
|
||||
<div class="toggle">
|
||||
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
|
||||
<label for="{{ tclass }}-delegate-duration"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group slide-duration-group">
|
||||
<label for="{{ tclass }}-duration">{{ l.slideshow_slide_form_label_duration }}</label>
|
||||
<div class="widget widget-unit">
|
||||
<input type="text" name="duration" id="{{ tclass }}-duration" required="required" value="3" min="0" class="numeric-input" />
|
||||
<span class="unit">{{ l.slideshow_slide_form_label_duration_unit }}</span>
|
||||
</div>
|
||||
<div class="form-group slide-duration-group">
|
||||
<label for="{{ tclass }}-duration">{{ l.slideshow_slide_form_label_duration }}</label>
|
||||
<div class="widget widget-unit">
|
||||
<input type="number" name="duration" id="{{ tclass }}-duration" required="required" value="3" min="0" />
|
||||
<span class="unit">{{ l.slideshow_slide_form_label_duration_unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<button type="button" class="btn btn-naked content-explr-picker">
|
||||
<i class="fa fa-crosshairs"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-neutral hidden slide-content-show" data-route="{{ url_for('slideshow_content_edit', content_id='__id__') }}">
|
||||
<button type="button" class="btn btn-neutral hidden slide-content-show" data-route="{{ url_for('slideshow_content_show', content_id='__id__') }}">
|
||||
<i class="fa-solid fa-up-right-from-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -86,22 +86,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="horizontal">
|
||||
<div class="form-group slide-delegate-duration-group">
|
||||
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
|
||||
<div class="widget">
|
||||
<div class="toggle">
|
||||
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
|
||||
<label for="{{ tclass }}-delegate-duration"></label>
|
||||
</div>
|
||||
<div class="form-group form-group-horizontal slide-delegate-duration-group">
|
||||
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
|
||||
<div class="widget">
|
||||
<div class="toggle">
|
||||
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
|
||||
<label for="{{ tclass }}-delegate-duration"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group slide-duration-group">
|
||||
<label for="{{ tclass }}-duration">{{ l.slideshow_slide_form_label_duration }}</label>
|
||||
<div class="widget widget-unit">
|
||||
<input type="text" name="duration" id="{{ tclass }}-duration" required="required" min="0" class="numeric-input" />
|
||||
<span class="unit">{{ l.slideshow_slide_form_label_duration_unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group slide-duration-group">
|
||||
<label for="{{ tclass }}-duration">{{ l.slideshow_slide_form_label_duration }}</label>
|
||||
<div class="widget widget-unit">
|
||||
<input type="number" name="duration" id="{{ tclass }}-duration" required="required" min="0" />
|
||||
<span class="unit">{{ l.slideshow_slide_form_label_duration_unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user