Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 883b5702b6 | |||
| 22ebe4939c | |||
| 601f493188 |
2
.github/workflows/build-nightly.yml
vendored
2
.github/workflows/build-nightly.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Call common build workflow
|
- name: Call common build workflow
|
||||||
uses: ./.github/actions/common-docker-build
|
uses: ./.github/actions/common-docker-build
|
||||||
with:
|
with:
|
||||||
build_tags: csmith1865/obscreen:nightly
|
build_tags: jierka/obscreen:nightly
|
||||||
manifest_tags: type=semver,pattern=nightly
|
manifest_tags: type=semver,pattern=nightly
|
||||||
flavor: ""
|
flavor: ""
|
||||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
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
|
- name: Call common build workflow
|
||||||
uses: ./.github/actions/common-docker-build
|
uses: ./.github/actions/common-docker-build
|
||||||
with:
|
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
|
manifest_tags: type=semver,pattern=pr
|
||||||
flavor: ""
|
flavor: ""
|
||||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
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:
|
with:
|
||||||
build_tags: |
|
build_tags: |
|
||||||
csmith1865/obscreen:v${{ steps.version.outputs.VERSION }}
|
csmith1865/obscreen:v${{ steps.version.outputs.VERSION }}
|
||||||
csmith1865/obscreen:latest
|
csmith1865/obscreen:latest
|
||||||
manifest_tags: type=semver,pattern=v${{ steps.version.outputs.VERSION }}
|
manifest_tags: type=semver,pattern=v${{ steps.version.outputs.VERSION }}
|
||||||
flavor: latest=true
|
flavor: latest=true
|
||||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
|||||||
20
README.md
20
README.md
@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
Obscreen is a user-friendly self-hosted digital signage tool leveraging chromium browser.
|
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)
|
🧑🎄 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
|
- Authentication management
|
||||||
- Plays content from flashdrive in offline mode
|
- Plays content from flashdrive in offline mode
|
||||||
- Core API & Plugin system to extend capabilities
|
- 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
|
- Cast pictures and iframes to Chromecast
|
||||||
- No costly monthly pricing plan per screen or whatever, no cloud, no telemetry
|
- 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:
|
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:
|
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:
|
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:
|
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
|
## 🫡 Motivation
|
||||||
|
|
||||||
@ -81,10 +81,10 @@ If you value this project, please think about awarding it a ⭐. Thanks ! 🙏
|
|||||||
## 🛟 Discussion / Need help ?
|
## 🛟 Discussion / Need help ?
|
||||||
|
|
||||||
### Join our Discord
|
### 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
|
### 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
|
### Troubleshoot
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ Check out the latest beta release here: https://github.com/jr-k/obscreen/release
|
|||||||
|
|
||||||
### Translations
|
### 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
|
### 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) {
|
always: function (e, data) {
|
||||||
const response = data._response.jqXHR;
|
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');
|
$button.removeClass('uploading').removeClass('btn-naked btn-super-upload-busy').addClass('btn-info btn-super-upload');
|
||||||
|
if (response.status != 200) {
|
||||||
let errorComment = response.responseText.match(/<!--\s*error=(\d+);\s*-->/);
|
const $alert = $('.alert-danger').removeClass('hidden');
|
||||||
if (errorComment && errorComment[1]) {
|
if (response.status == 413) {
|
||||||
statusCode = parseInt(errorComment[1], 10);
|
$alert.text(l.js_common_http_error_413);
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
document.location.reload();
|
document.location.reload();
|
||||||
@ -30,10 +30,6 @@ const hideDropdowns = function () {
|
|||||||
$('.dropdown').removeClass('dropdown-show');
|
$('.dropdown').removeClass('dropdown-show');
|
||||||
};
|
};
|
||||||
|
|
||||||
const classColorXor = function(color, fallback) {
|
|
||||||
return color === 'gscaleF' ? 'gscale0' : (color === 'gscale0' ? 'gscaleF' : fallback);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showToast = function (text) {
|
const showToast = function (text) {
|
||||||
const $toast = $(".toast");
|
const $toast = $(".toast");
|
||||||
$toast.addClass('show');
|
$toast.addClass('show');
|
||||||
@ -166,12 +162,5 @@ jQuery(document).ready(function ($) {
|
|||||||
|
|
||||||
showToast(l.js_common_copied);
|
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);
|
$form.find('.object-label:visible').html(optionAttributes['data-object-label'].value);
|
||||||
$('.type-icon').attr('class', 'type-icon fa ' + optionAttributes['data-icon'].value);
|
$('.type-icon').attr('class', 'type-icon fa ' + optionAttributes['data-icon'].value);
|
||||||
$('.tab-select .widget').attr('class', 'widget ' + ('border-' + color) + ' ' + color);
|
$('.tab-select .widget').attr('class', 'widget ' + ('border-' + color) + ' ' + color);
|
||||||
$form.find('button[type=submit]').attr('class', [
|
$form.find('button[type=submit]').attr('class', 'btn ' + ('btn-' + color));
|
||||||
'btn',
|
|
||||||
`btn-${color}`,
|
|
||||||
classColorXor(color, '')
|
|
||||||
].join(' '));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = function () {
|
const main = function () {
|
||||||
|
|||||||
@ -81,9 +81,3 @@ const secondsToHHMMSS = function (seconds) {
|
|||||||
const secs = seconds % 60;
|
const secs = seconds % 60;
|
||||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
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;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-tail {
|
.contex-tail {
|
||||||
margin-right: 30px;
|
margin-right: 20px;
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-tail-auth {
|
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|||||||
@ -26,10 +26,6 @@ body, html {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
|
|
||||||
&.fx-end {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical {
|
.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 {
|
a.badge,
|
||||||
display: inline;
|
.badge {
|
||||||
color: $gscaleA;
|
display: flex;
|
||||||
font-size: 12px;
|
flex-direction: row;
|
||||||
margin-left: 5px;
|
justify-content: center;
|
||||||
background: $gscale0;
|
align-items: center;
|
||||||
border: 1px solid $gscale3;
|
padding: 5px 5px;
|
||||||
border-radius: $baseRadius;
|
border-radius: $baseRadius;
|
||||||
padding: 3px 7px;
|
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,
|
button,
|
||||||
.btn {
|
.btn {
|
||||||
$shadowOffset: 2px;
|
$shadowOffset: 2px;
|
||||||
@ -57,7 +56,6 @@ button,
|
|||||||
box-shadow: 0 $shadowOffset 0 0 darken($gscale5, 10%);
|
box-shadow: 0 $shadowOffset 0 0 darken($gscale5, 10%);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 $shadowOffset 0 1px $gkscale2 inset;
|
box-shadow: 0 $shadowOffset 0 1px $gkscale2 inset;
|
||||||
background: darken($gscale5, 10%);
|
background: darken($gscale5, 10%);
|
||||||
@ -143,3 +141,4 @@ button,
|
|||||||
cursor: default;
|
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 {
|
.widget {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
@ -118,11 +100,12 @@ form {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input + .btn + .btn {
|
.btn {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.widget-unit {
|
&.widget-unit {
|
||||||
|
|
||||||
select,
|
select,
|
||||||
input {
|
input {
|
||||||
flex-grow: 0;
|
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 {
|
div {
|
||||||
color: rgba($gscaleF, .7);
|
color: rgba($gscaleF, .7);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -199,17 +155,23 @@ form {
|
|||||||
color: $gscale5;
|
color: $gscale5;
|
||||||
background: none;
|
background: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid $gscale3;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.input-naked {
|
&.input-naked {
|
||||||
|
padding-left: 0;
|
||||||
color: $gscaleB;
|
color: $gscaleB;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled,
|
&.disabled,
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
|
border: none;
|
||||||
background: $gscale0;
|
background: $gscale0;
|
||||||
border-radius: $baseRadius;
|
border-radius: $baseRadius;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,20 +18,19 @@
|
|||||||
@import 'components/modals';
|
@import 'components/modals';
|
||||||
@import 'components/toast';
|
@import 'components/toast';
|
||||||
@import 'components/dragdrop';
|
@import 'components/dragdrop';
|
||||||
@import 'components/animation';
|
|
||||||
|
// Legacy
|
||||||
@import 'components/panes';
|
@import 'components/panes';
|
||||||
@import 'components/tiles';
|
@import 'components/tiles';
|
||||||
@import 'components/empty';
|
@import 'components/empty';
|
||||||
@import 'components/switches';
|
@import 'components/switches';
|
||||||
@import 'components/badges';
|
//@import 'components/badges';
|
||||||
|
|
||||||
// Import form styles
|
// Import form styles
|
||||||
@import 'forms/forms';
|
@import 'forms/forms';
|
||||||
|
|
||||||
// Import pages styles
|
// Import pages styles
|
||||||
@import 'pages/content';
|
@import 'pages/content';
|
||||||
@import 'pages/content-composition';
|
|
||||||
@import 'pages/content-text';
|
|
||||||
@import 'pages/logs';
|
@import 'pages/logs';
|
||||||
@import 'pages/node-player';
|
@import 'pages/node-player';
|
||||||
@import 'pages/playlist';
|
@import 'pages/playlist';
|
||||||
|
|||||||
@ -43,25 +43,6 @@ button,
|
|||||||
&.btn-neutral:hover {
|
&.btn-neutral:hover {
|
||||||
box-shadow: 0 2px 0 1px $gkscale6 inset;
|
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 {
|
.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 {
|
.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 {
|
.bottom-content {
|
||||||
.page-content {
|
.page-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -61,11 +38,32 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
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 {
|
.iframe-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -89,3 +87,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ $layoutBorder: 1px solid $gscale2;
|
|||||||
// Packs
|
// Packs
|
||||||
$colors: (
|
$colors: (
|
||||||
warning: $warning,
|
warning: $warning,
|
||||||
orange: $orange,
|
|
||||||
info: $info,
|
info: $info,
|
||||||
info-alt: $bitterBlue,
|
info-alt: $bitterBlue,
|
||||||
success: $success,
|
success: $success,
|
||||||
@ -40,8 +39,6 @@ $colors: (
|
|||||||
redhat:$redhat,
|
redhat:$redhat,
|
||||||
centos:$centos,
|
centos:$centos,
|
||||||
other:$other,
|
other:$other,
|
||||||
gscale0:$gscale0,
|
|
||||||
gscaleF:$gscaleF,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
|
|||||||
@ -2,7 +2,7 @@ services:
|
|||||||
webapp:
|
webapp:
|
||||||
container_name: obscreen
|
container_name: obscreen
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: csmith1865/obscreen:latest
|
image: jierka/obscreen:latest
|
||||||
environment:
|
environment:
|
||||||
- DEMO=false
|
- DEMO=false
|
||||||
- DEBUG=false
|
- DEBUG=false
|
||||||
|
|||||||
@ -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.
|
#### 🔴 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.
|
||||||
|
|
||||||
@ -22,12 +22,12 @@
|
|||||||
|
|
||||||
##### Linux
|
##### Linux
|
||||||
```bash
|
```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
|
sudo reboot
|
||||||
```
|
```
|
||||||
##### Windows & MacOS
|
##### Windows & MacOS
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/csmith1865/obscreen.git
|
git clone https://github.com/jr-k/obscreen.git
|
||||||
cd obscreen
|
cd obscreen
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source ./venv/bin/activate
|
source ./venv/bin/activate
|
||||||
@ -82,7 +82,7 @@ docker run --restart=always --name obscreen --pull=always \
|
|||||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||||
|
|
||||||
# Download docker-compose.yml
|
# 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
|
# Run
|
||||||
docker compose up --detach --pull=always
|
docker compose up --detach --pull=always
|
||||||
@ -106,7 +106,7 @@ docker compose up --detach --pull=always
|
|||||||
#### How to install
|
#### How to install
|
||||||
- Install player autorun by executing following script (will install chromium, x11, pulseaudio and obscreen-player systemd service)
|
- Install player autorun by executing following script (will install chromium, x11, pulseaudio and obscreen-player systemd service)
|
||||||
```bash
|
```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
|
sudo reboot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
23
lang/en.json
23
lang/en.json
@ -59,7 +59,7 @@
|
|||||||
"js_slideshow_slide_delete_confirmation": "Are you sure?",
|
"js_slideshow_slide_delete_confirmation": "Are you sure?",
|
||||||
"slideshow_content_page_title": "Content Library",
|
"slideshow_content_page_title": "Content Library",
|
||||||
"slideshow_content_button_add": "New Content",
|
"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_active": "Content",
|
||||||
"slideshow_content_panel_empty": "Currently, there are no content. %link% now.",
|
"slideshow_content_panel_empty": "Currently, there are no content. %link% now.",
|
||||||
"slideshow_content_panel_th_name": "Name",
|
"slideshow_content_panel_th_name": "Name",
|
||||||
@ -255,27 +255,12 @@
|
|||||||
"common_apply": "Apply",
|
"common_apply": "Apply",
|
||||||
"common_saved": "Changes have been saved",
|
"common_saved": "Changes have been saved",
|
||||||
"common_new_folder": "New Folder",
|
"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_copied": "Element copied in clipboard!",
|
||||||
"common_host_placeholder": "raspberrypi.local or 192.168.1.85",
|
"common_host_placeholder": "raspberrypi.local or 192.168.1.85",
|
||||||
"common_reachable_at": "Host",
|
"common_reachable_at": "Host",
|
||||||
"common_http_error_occured": "Error %code% occured",
|
"common_http_error_occured": "Error %code% occured",
|
||||||
"common_http_error_413": "Files are too large",
|
"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",
|
"logout": "Logout",
|
||||||
"login_error_not_found": "Bad credentials",
|
"login_error_not_found": "Bad credentials",
|
||||||
"login_error_bad_credentials": "Bad credentials",
|
"login_error_bad_credentials": "Bad credentials",
|
||||||
@ -307,10 +292,6 @@
|
|||||||
"enum_content_type_external_storage": "External Storage",
|
"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_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_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_url": "URL",
|
||||||
"enum_content_type_video": "Video",
|
"enum_content_type_video": "Video",
|
||||||
"enum_content_type_picture": "Picture",
|
"enum_content_type_picture": "Picture",
|
||||||
|
|||||||
23
lang/es.json
23
lang/es.json
@ -59,7 +59,7 @@
|
|||||||
"js_slideshow_slide_delete_confirmation": "¿Estás seguro?",
|
"js_slideshow_slide_delete_confirmation": "¿Estás seguro?",
|
||||||
"slideshow_content_page_title": "Biblioteca de contenidos",
|
"slideshow_content_page_title": "Biblioteca de contenidos",
|
||||||
"slideshow_content_button_add": "Nuevo Contenido",
|
"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_active": "Contenido",
|
||||||
"slideshow_content_panel_empty": "Actualmente, no hay contenido. %link% ahora.",
|
"slideshow_content_panel_empty": "Actualmente, no hay contenido. %link% ahora.",
|
||||||
"slideshow_content_panel_th_name": "Nombre",
|
"slideshow_content_panel_th_name": "Nombre",
|
||||||
@ -256,27 +256,12 @@
|
|||||||
"common_apply": "Aplicar",
|
"common_apply": "Aplicar",
|
||||||
"common_saved": "Los cambios se han guardado",
|
"common_saved": "Los cambios se han guardado",
|
||||||
"common_new_folder": "Nuevo Carpeta",
|
"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_copied": "¡Elemento copiado!",
|
||||||
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
||||||
"common_reachable_at": "Host",
|
"common_reachable_at": "Host",
|
||||||
"common_http_error_occured": "Se ha producido un error %code%",
|
"common_http_error_occured": "Se ha producido un error %code%",
|
||||||
"common_http_error_413": "Los archivos son demasiado grandes",
|
"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",
|
"logout": "Cerrar sesión",
|
||||||
"login_error_not_found": "Credenciales incorrectas",
|
"login_error_not_found": "Credenciales incorrectas",
|
||||||
"login_error_bad_credentials": "Credenciales incorrectas",
|
"login_error_bad_credentials": "Credenciales incorrectas",
|
||||||
@ -308,10 +293,6 @@
|
|||||||
"enum_content_type_external_storage": "Almacenamiento externo",
|
"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_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_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_url": "URL",
|
||||||
"enum_content_type_video": "Video",
|
"enum_content_type_video": "Video",
|
||||||
"enum_content_type_picture": "Imagen",
|
"enum_content_type_picture": "Imagen",
|
||||||
|
|||||||
23
lang/fr.json
23
lang/fr.json
@ -59,7 +59,7 @@
|
|||||||
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
|
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
|
||||||
"slideshow_content_page_title": "Bibliothèque de contenus",
|
"slideshow_content_page_title": "Bibliothèque de contenus",
|
||||||
"slideshow_content_button_add": "Nouveau Contenu",
|
"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_active": "Contenus",
|
||||||
"slideshow_content_panel_empty": "Actuellement, il n'y a aucun contenu. %link% maintenant.",
|
"slideshow_content_panel_empty": "Actuellement, il n'y a aucun contenu. %link% maintenant.",
|
||||||
"slideshow_content_panel_th_name": "Nom",
|
"slideshow_content_panel_th_name": "Nom",
|
||||||
@ -257,27 +257,12 @@
|
|||||||
"common_apply": "Appliquer",
|
"common_apply": "Appliquer",
|
||||||
"common_saved": "Les modifications ont été enregistrées",
|
"common_saved": "Les modifications ont été enregistrées",
|
||||||
"common_new_folder": "Nouveau Dossier",
|
"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_copied": "Element copié !",
|
||||||
"common_host_placeholder": "raspberrypi.local ou 192.168.1.85",
|
"common_host_placeholder": "raspberrypi.local ou 192.168.1.85",
|
||||||
"common_reachable_at": "Hôte",
|
"common_reachable_at": "Hôte",
|
||||||
"common_http_error_occured": "Une erreur %code% est apparue",
|
"common_http_error_occured": "Une erreur %code% est apparue",
|
||||||
"common_http_error_413": "Les fichiers sont trop volumineux",
|
"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",
|
"logout": "Déconnexion",
|
||||||
"login_error_not_found": "Identifiants invalides",
|
"login_error_not_found": "Identifiants invalides",
|
||||||
"login_error_bad_credentials": "Identifiants invalides",
|
"login_error_bad_credentials": "Identifiants invalides",
|
||||||
@ -309,10 +294,6 @@
|
|||||||
"enum_content_type_external_storage": "Stockage externe",
|
"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_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_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_url": "URL",
|
||||||
"enum_content_type_video": "Vidéo",
|
"enum_content_type_video": "Vidéo",
|
||||||
"enum_content_type_picture": "Image",
|
"enum_content_type_picture": "Image",
|
||||||
|
|||||||
23
lang/it.json
23
lang/it.json
@ -59,7 +59,7 @@
|
|||||||
"js_slideshow_slide_delete_confirmation": "Sei sicuro?",
|
"js_slideshow_slide_delete_confirmation": "Sei sicuro?",
|
||||||
"slideshow_content_page_title": "Libreria dei contenuti",
|
"slideshow_content_page_title": "Libreria dei contenuti",
|
||||||
"slideshow_content_button_add": "Nuovo Contenuto",
|
"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_active": "Contenuti",
|
||||||
"slideshow_content_panel_empty": "Attualmente non ci sono contenuti. %link% adesso.",
|
"slideshow_content_panel_empty": "Attualmente non ci sono contenuti. %link% adesso.",
|
||||||
"slideshow_content_panel_th_name": "Nome",
|
"slideshow_content_panel_th_name": "Nome",
|
||||||
@ -256,27 +256,12 @@
|
|||||||
"common_apply": "Applica",
|
"common_apply": "Applica",
|
||||||
"common_saved": "Le modifiche sono state salvate",
|
"common_saved": "Le modifiche sono state salvate",
|
||||||
"common_new_folder": "Nuovo Cartella",
|
"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_copied": "Elemento copiato!",
|
||||||
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
||||||
"common_reachable_at": "Host",
|
"common_reachable_at": "Host",
|
||||||
"common_http_error_occured": "Si è verificato un errore %code%",
|
"common_http_error_occured": "Si è verificato un errore %code%",
|
||||||
"common_http_error_413": "I file sono troppo grandi",
|
"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",
|
"logout": "Logout",
|
||||||
"login_error_not_found": "Credenziali errate",
|
"login_error_not_found": "Credenziali errate",
|
||||||
"login_error_bad_credentials": "Credenziali errate",
|
"login_error_bad_credentials": "Credenziali errate",
|
||||||
@ -308,10 +293,6 @@
|
|||||||
"enum_content_type_external_storage": "Archiviazione esterna",
|
"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_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_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_url": "URL",
|
||||||
"enum_content_type_video": "Video",
|
"enum_content_type_video": "Video",
|
||||||
"enum_content_type_picture": "Immagine",
|
"enum_content_type_picture": "Immagine",
|
||||||
|
|||||||
@ -22,7 +22,8 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block top_page %}
|
{% block page %}
|
||||||
|
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
|
|
||||||
@ -31,9 +32,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main_page %}
|
|
||||||
<div class="bottom-content">
|
<div class="bottom-content">
|
||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import json
|
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 flask_login import login_user, logout_user, current_user
|
||||||
from src.service.ModelStore import ModelStore
|
from src.service.ModelStore import ModelStore
|
||||||
from src.model.entity.User import User
|
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'])
|
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):
|
def login(self):
|
||||||
|
login_error = None
|
||||||
|
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('playlist'))
|
return redirect(url_for('playlist'))
|
||||||
|
|
||||||
@ -39,12 +41,13 @@ class AuthController(ObController):
|
|||||||
login_user(user)
|
login_user(user)
|
||||||
return redirect(url_for('playlist'))
|
return redirect(url_for('playlist'))
|
||||||
else:
|
else:
|
||||||
flash(self.t('login_error_bad_credentials'), 'error')
|
login_error = 'bad_credentials'
|
||||||
else:
|
else:
|
||||||
flash(self.t('login_error_not_found'), 'error')
|
login_error = 'not_found'
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'auth/login.jinja.html',
|
'auth/login.jinja.html',
|
||||||
|
login_error=login_error,
|
||||||
last_username=request.form['username'] if 'username' in request.form else None
|
last_username=request.form['username'] if 'username' in request.form else None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -64,6 +67,7 @@ class AuthController(ObController):
|
|||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'auth/list.jinja.html',
|
'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),
|
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()
|
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'))
|
return redirect(url_for('auth_user_list'))
|
||||||
|
|
||||||
if user.id == str(current_user.id):
|
if user.id == str(current_user.id):
|
||||||
flash(self.t('auth_user_delete_cant_delete_yourself'), 'error')
|
return redirect(url_for('auth_user_list', error='auth_user_delete_cant_delete_yourself'))
|
||||||
return redirect(url_for('auth_user_list'))
|
|
||||||
|
|
||||||
if self._model_store.user().count_all_enabled() == 1:
|
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', error='auth_user_delete_at_least_one_account'))
|
||||||
return redirect(url_for('auth_user_list'))
|
|
||||||
|
|
||||||
self._model_store.user().delete(user_id)
|
self._model_store.user().delete(user_id)
|
||||||
return redirect(url_for('auth_user_list'))
|
return redirect(url_for('auth_user_list'))
|
||||||
|
|||||||
@ -2,12 +2,11 @@ import json
|
|||||||
import os
|
import os
|
||||||
import time
|
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 werkzeug.utils import secure_filename
|
||||||
from src.service.ModelStore import ModelStore
|
from src.service.ModelStore import ModelStore
|
||||||
from src.model.entity.Content import Content
|
from src.model.entity.Content import Content
|
||||||
from src.model.enum.ContentType import ContentType
|
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.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
|
||||||
from src.interface.ObController import ObController
|
from src.interface.ObController import ObController
|
||||||
from src.util.utils import str_to_enum, get_optional_string
|
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/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/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/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/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'])
|
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:
|
if not content:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
|
||||||
vargs = {}
|
|
||||||
working_folder_path, working_folder = self.get_folder_context()
|
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(
|
return render_template(
|
||||||
edit_view,
|
'slideshow/contents/edit.jinja.html',
|
||||||
content=content,
|
content=content,
|
||||||
working_folder_path=working_folder_path,
|
working_folder_path=working_folder_path,
|
||||||
working_folder=working_folder,
|
working_folder=working_folder,
|
||||||
enum_content_type=ContentType,
|
enum_content_type=ContentType,
|
||||||
enum_content_metadata=ContentMetadata,
|
external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint')
|
||||||
external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint'),
|
|
||||||
**vargs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def slideshow_content_save(self, content_id: int = 0):
|
def slideshow_content_save(self, content_id: int = 0):
|
||||||
@ -146,19 +135,17 @@ class ContentController(ObController):
|
|||||||
)
|
)
|
||||||
self._post_update()
|
self._post_update()
|
||||||
|
|
||||||
flash(self.t('common_saved'), 'success')
|
return redirect(url_for('slideshow_content_edit', content_id=content_id, saved=1))
|
||||||
|
|
||||||
return redirect(url_for('slideshow_content_edit', content_id=content_id))
|
|
||||||
|
|
||||||
def slideshow_content_delete(self):
|
def slideshow_content_delete(self):
|
||||||
working_folder_path, working_folder = self.get_folder_context()
|
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 = {
|
route_args = {
|
||||||
"path": working_folder_path,
|
"path": working_folder_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
if error:
|
if error_tuple:
|
||||||
flash(error, 'error')
|
route_args[error_tuple[0]] = error_tuple[1]
|
||||||
|
|
||||||
return redirect(url_for('slideshow_content_list', **route_args))
|
return redirect(url_for('slideshow_content_list', **route_args))
|
||||||
|
|
||||||
@ -238,16 +225,24 @@ class ContentController(ObController):
|
|||||||
|
|
||||||
def slideshow_content_folder_delete(self):
|
def slideshow_content_folder_delete(self):
|
||||||
working_folder_path, working_folder = self.get_folder_context()
|
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 = {
|
route_args = {
|
||||||
"path": working_folder_path,
|
"path": working_folder_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
if error:
|
if error_tuple:
|
||||||
flash(self.t(error), 'error')
|
route_args[error_tuple[0]] = error_tuple[1]
|
||||||
|
|
||||||
return redirect(url_for('slideshow_content_list', **route_args))
|
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):
|
def slideshow_content_delete_bulk_explr(self):
|
||||||
working_folder_path, working_folder = self.get_folder_context()
|
working_folder_path, working_folder = self.get_folder_context()
|
||||||
entity_ids = request.args.get('entity_ids', '').split(',')
|
entity_ids = request.args.get('entity_ids', '').split(',')
|
||||||
@ -256,17 +251,17 @@ class ContentController(ObController):
|
|||||||
|
|
||||||
for id in entity_ids:
|
for id in entity_ids:
|
||||||
if id:
|
if id:
|
||||||
error = self.delete_content_by_id(id)
|
error_tuple = self.delete_content_by_id(id)
|
||||||
|
|
||||||
if error:
|
if error_tuple:
|
||||||
flash(error, 'error')
|
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||||
|
|
||||||
for id in folder_ids:
|
for id in folder_ids:
|
||||||
if id:
|
if id:
|
||||||
error = self.delete_folder_by_id(id)
|
error_tuple = self.delete_folder_by_id(id)
|
||||||
|
|
||||||
if error:
|
if error_tuple:
|
||||||
flash(error, 'error')
|
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||||
|
|
||||||
return redirect(url_for('slideshow_content_list', **route_args_dict))
|
return redirect(url_for('slideshow_content_list', **route_args_dict))
|
||||||
|
|
||||||
@ -277,7 +272,7 @@ class ContentController(ObController):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if self._model_store.slide().count_slides_for_content(content.id) > 0:
|
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._model_store.content().delete(content.id)
|
||||||
self._post_update()
|
self._post_update()
|
||||||
@ -293,7 +288,7 @@ class ContentController(ObController):
|
|||||||
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
|
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
|
||||||
|
|
||||||
if content_counter > 0 or folder_counter:
|
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._model_store.folder().delete(id=folder.id)
|
||||||
self._post_update()
|
self._post_update()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import json
|
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.service.ModelStore import ModelStore
|
||||||
from src.model.entity.NodePlayer import NodePlayer
|
from src.model.entity.NodePlayer import NodePlayer
|
||||||
from src.interface.ObController import ObController
|
from src.interface.ObController import ObController
|
||||||
@ -108,19 +108,18 @@ class FleetNodePlayerController(ObController):
|
|||||||
)
|
)
|
||||||
self._post_update()
|
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))
|
return redirect(url_for('fleet_node_player_list', path=working_folder_path))
|
||||||
|
|
||||||
def fleet_node_player_delete(self):
|
def fleet_node_player_delete(self):
|
||||||
working_folder_path, working_folder = self.get_working_folder()
|
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 = {
|
route_args = {
|
||||||
"path": working_folder_path,
|
"path": working_folder_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
if error:
|
if error_tuple:
|
||||||
flash(error, 'error')
|
route_args[error_tuple[0]] = error_tuple[1]
|
||||||
|
|
||||||
return redirect(url_for('fleet_node_player_list', **route_args))
|
return redirect(url_for('fleet_node_player_list', **route_args))
|
||||||
|
|
||||||
@ -201,13 +200,13 @@ class FleetNodePlayerController(ObController):
|
|||||||
|
|
||||||
def fleet_node_player_folder_delete(self):
|
def fleet_node_player_folder_delete(self):
|
||||||
working_folder_path, working_folder = self.get_working_folder()
|
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 = {
|
route_args = {
|
||||||
"path": working_folder_path,
|
"path": working_folder_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
if error:
|
if error_tuple:
|
||||||
flash(error, 'error')
|
route_args[error_tuple[0]] = error_tuple[1]
|
||||||
|
|
||||||
return redirect(url_for('fleet_node_player_list', **route_args))
|
return redirect(url_for('fleet_node_player_list', **route_args))
|
||||||
|
|
||||||
@ -219,17 +218,17 @@ class FleetNodePlayerController(ObController):
|
|||||||
|
|
||||||
for id in entity_ids:
|
for id in entity_ids:
|
||||||
if id:
|
if id:
|
||||||
error = self.delete_node_player_by_id(id)
|
error_tuple = self.delete_node_player_by_id(id)
|
||||||
|
|
||||||
if error:
|
if error_tuple:
|
||||||
flash(error, 'error')
|
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||||
|
|
||||||
for id in folder_ids:
|
for id in folder_ids:
|
||||||
if id:
|
if id:
|
||||||
error = self.delete_folder_by_id(id)
|
error_tuple = self.delete_folder_by_id(id)
|
||||||
|
|
||||||
if error:
|
if error_tuple:
|
||||||
flash(error, 'error')
|
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||||
|
|
||||||
return redirect(url_for('fleet_node_player_list', **route_args_dict))
|
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)
|
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
|
||||||
|
|
||||||
if node_player_counter > 0 or folder_counter:
|
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)
|
self._model_store.folder().delete(id=folder.id)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import json
|
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.service.ModelStore import ModelStore
|
||||||
from src.model.entity.NodePlayerGroup import NodePlayerGroup
|
from src.model.entity.NodePlayerGroup import NodePlayerGroup
|
||||||
from src.model.enum.FolderEntity import FolderEntity
|
from src.model.enum.FolderEntity import FolderEntity
|
||||||
@ -43,6 +43,7 @@ class FleetNodePlayerGroupController(ObController):
|
|||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'fleet/player-group/list.jinja.html',
|
'fleet/player-group/list.jinja.html',
|
||||||
|
error=request.args.get('error', None),
|
||||||
current_player_group=current_player_group,
|
current_player_group=current_player_group,
|
||||||
node_player_groups=node_player_groups,
|
node_player_groups=node_player_groups,
|
||||||
pcounters=pcounters,
|
pcounters=pcounters,
|
||||||
@ -85,8 +86,7 @@ class FleetNodePlayerGroupController(ObController):
|
|||||||
|
|
||||||
def fleet_node_player_group_delete(self, player_group_id: int):
|
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:
|
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, error='node_player_group_delete_has_node_player'))
|
||||||
return redirect(url_for('fleet_node_player_group_list', player_group_id=player_group_id))
|
|
||||||
|
|
||||||
self._model_store.node_player_group().delete(player_group_id)
|
self._model_store.node_player_group().delete(player_group_id)
|
||||||
return redirect(url_for('fleet_node_player_group'))
|
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', '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('/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/<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 = ''):
|
def player(self, playlist_slug_or_id: str = ''):
|
||||||
preview_playlist = request.args.get('preview_playlist')
|
preview_playlist = request.args.get('preview_playlist')
|
||||||
preview_content_id = request.args.get('preview_content_id')
|
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)
|
playlist_slug_or_id = self._get_dynamic_playlist_id(playlist_slug_or_id)
|
||||||
|
|
||||||
if not preview_content_id:
|
query = " (slug = ? OR id = ?) "
|
||||||
query = " (slug = ? OR id = ?) "
|
query_args = {
|
||||||
query_args = {
|
"slug": playlist_slug_or_id,
|
||||||
"slug": playlist_slug_or_id,
|
"id": playlist_slug_or_id,
|
||||||
"id": playlist_slug_or_id,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if not preview_playlist:
|
if not preview_playlist:
|
||||||
query = query + " AND enabled = ? "
|
query = query + " AND enabled = ? "
|
||||||
query_args["enabled"] = True
|
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:
|
if playlist_slug_or_id and not current_playlist:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
|
||||||
playlist_id = current_playlist.id if current_playlist else None
|
playlist_id = current_playlist.id if current_playlist else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
items = self._get_playlist(playlist_id=playlist_id, preview_content_id=preview_content_id)
|
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_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())
|
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(
|
return render_template(
|
||||||
'player/player.jinja.html',
|
'player/player.jinja.html',
|
||||||
items=items,
|
items=items,
|
||||||
@ -127,7 +122,7 @@ class PlayerController(ObController):
|
|||||||
preview_content = self._model_store.content().get(preview_content_id) if preview_content_id else None
|
preview_content = self._model_store.content().get(preview_content_id) if preview_content_id else None
|
||||||
preview_mode = preview_content is not 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")
|
playlist = self._model_store.playlist().get_one_by(query="fallback = 1")
|
||||||
|
|
||||||
if playlist:
|
if playlist:
|
||||||
@ -137,9 +132,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)
|
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)
|
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()
|
||||||
contents = self._model_store.content().get_all_indexed(query="id IN ({})".format(','.join(content_ids)))
|
playlist = self._model_store.playlist().get(playlist_id)
|
||||||
playlist = self._model_store.playlist().get(playlist_id) if not preview_mode else None
|
|
||||||
position = 9999
|
position = 9999
|
||||||
|
|
||||||
playlist_loop = []
|
playlist_loop = []
|
||||||
@ -254,14 +248,3 @@ class PlayerController(ObController):
|
|||||||
response.headers['ETag'] = etag
|
response.headers['ETag'] = etag
|
||||||
|
|
||||||
return response
|
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
|
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.service.ModelStore import ModelStore
|
||||||
from src.model.entity.Playlist import Playlist
|
from src.model.entity.Playlist import Playlist
|
||||||
from src.model.enum.FolderEntity import FolderEntity
|
from src.model.enum.FolderEntity import FolderEntity
|
||||||
@ -35,6 +35,7 @@ class PlaylistController(ObController):
|
|||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'playlist/list.jinja.html',
|
'playlist/list.jinja.html',
|
||||||
|
error=request.args.get('error', None),
|
||||||
current_playlist=current_playlist,
|
current_playlist=current_playlist,
|
||||||
playlists=playlists,
|
playlists=playlists,
|
||||||
durations=durations,
|
durations=durations,
|
||||||
@ -82,12 +83,10 @@ class PlaylistController(ObController):
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if self._model_store.slide().count_slides_for_playlist(playlist_id) > 0:
|
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, error='playlist_delete_has_slides'))
|
||||||
return redirect(url_for('playlist_list', playlist_id=playlist_id))
|
|
||||||
|
|
||||||
if self._model_store.node_player_group().count_node_player_groups_for_playlist(playlist_id) > 0:
|
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, error='playlist_delete_has_node_player_groups'))
|
||||||
return redirect(url_for('playlist_list', playlist_id=playlist_id))
|
|
||||||
|
|
||||||
self._model_store.playlist().delete(playlist_id)
|
self._model_store.playlist().delete(playlist_id)
|
||||||
return redirect(url_for('playlist'))
|
return redirect(url_for('playlist'))
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import time
|
|||||||
import json
|
import json
|
||||||
import threading
|
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 typing import Optional
|
||||||
|
|
||||||
from src.service.ModelStore import ModelStore
|
from src.service.ModelStore import ModelStore
|
||||||
@ -40,8 +40,7 @@ class SettingsController(ObController):
|
|||||||
error = self._pre_update(request.form['id'])
|
error = self._pre_update(request.form['id'])
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
flash(error, 'error')
|
return redirect(url_for('settings_variable_list', error=error))
|
||||||
return redirect(url_for('settings_variable_list'))
|
|
||||||
|
|
||||||
self._model_store.variable().update_form(request.form['id'], request.form['value'])
|
self._model_store.variable().update_form(request.form['id'], request.form['value'])
|
||||||
redirect_response = self._post_update(request.form['id'])
|
redirect_response = self._post_update(request.form['id'])
|
||||||
@ -55,8 +54,7 @@ class SettingsController(ObController):
|
|||||||
error = self._pre_update(request.form['id'])
|
error = self._pre_update(request.form['id'])
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
flash(error, 'error')
|
return redirect(url_for('settings_variable_plugin_list', error=error))
|
||||||
return redirect(url_for('settings_variable_plugin_list'))
|
|
||||||
|
|
||||||
self._model_store.variable().update_form(request.form['id'], request.form['value'])
|
self._model_store.variable().update_form(request.form['id'], request.form['value'])
|
||||||
redirect_response = self._post_update(request.form['id'])
|
redirect_response = self._post_update(request.form['id'])
|
||||||
@ -81,8 +79,7 @@ class SettingsController(ObController):
|
|||||||
|
|
||||||
if variable.name == 'slide_upload_limit':
|
if variable.name == 'slide_upload_limit':
|
||||||
self.reload_web_server()
|
self.reload_web_server()
|
||||||
flash(self.t('common_restart_needed'), 'warning')
|
return redirect(url_for('settings_variable_list', warning='common_restart_needed'))
|
||||||
return redirect(url_for('settings_variable_list'))
|
|
||||||
|
|
||||||
if variable.name == 'fleet_player_enabled':
|
if variable.name == 'fleet_player_enabled':
|
||||||
self.reload_web_server()
|
self.reload_web_server()
|
||||||
@ -101,10 +98,7 @@ class SettingsController(ObController):
|
|||||||
thread = threading.Thread(target=self.plugin_update)
|
thread = threading.Thread(target=self.plugin_update)
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
flash(self.t('common_restart_needed'), 'warning')
|
return redirect(url_for('settings_variable_plugin_list', warning='common_restart_needed'))
|
||||||
return redirect(url_for('settings_variable_plugin_list'))
|
|
||||||
|
|
||||||
flash(self.t('common_saved'), 'success')
|
|
||||||
|
|
||||||
def plugin_update(self) -> None:
|
def plugin_update(self) -> None:
|
||||||
restart()
|
restart()
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import time
|
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 werkzeug.utils import secure_filename
|
||||||
from src.service.ModelStore import ModelStore
|
from src.service.ModelStore import ModelStore
|
||||||
from src.model.entity.Slide import Slide
|
from src.model.entity.Slide import Slide
|
||||||
@ -102,11 +102,11 @@ class SlideController(ObController):
|
|||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
def slideshow_player_refresh(self):
|
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())
|
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):
|
def _post_update(self):
|
||||||
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import abc
|
import abc
|
||||||
|
|
||||||
from typing import Optional, List, Dict, Union
|
from typing import Optional, List, Dict, Union
|
||||||
from flask import request
|
|
||||||
from src.service.TemplateRenderer import TemplateRenderer
|
from src.service.TemplateRenderer import TemplateRenderer
|
||||||
from src.service.ModelStore import ModelStore
|
from src.service.ModelStore import ModelStore
|
||||||
from src.interface.ObPlugin import ObPlugin
|
from src.interface.ObPlugin import ObPlugin
|
||||||
@ -52,19 +51,3 @@ class ObController(abc.ABC):
|
|||||||
|
|
||||||
def api(self):
|
def api(self):
|
||||||
return self._web_server.api
|
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.Content import Content
|
||||||
from src.model.entity.Playlist import Playlist
|
from src.model.entity.Playlist import Playlist
|
||||||
from src.model.enum.ContentMetadata import ContentMetadata
|
|
||||||
from src.model.enum.ContentType import ContentType
|
from src.model.enum.ContentType import ContentType
|
||||||
from src.util.utils import get_yt_video_id
|
from src.util.utils import get_yt_video_id
|
||||||
from src.manager.DatabaseManager import DatabaseManager
|
from src.manager.DatabaseManager import DatabaseManager
|
||||||
@ -17,8 +16,7 @@ from src.manager.VariableManager import VariableManager
|
|||||||
from src.service.ModelManager import ModelManager
|
from src.service.ModelManager import ModelManager
|
||||||
from src.util.UtilFile import randomize_filename
|
from src.util.UtilFile import randomize_filename
|
||||||
from src.util.UtilNetwork import get_preferred_ip_address
|
from src.util.UtilNetwork import get_preferred_ip_address
|
||||||
from src.util.UtilVideo import get_video_metadata
|
from src.util.UtilVideo import mp4_duration_with_ffprobe
|
||||||
from src.util.UtilPicture import get_picture_metadata
|
|
||||||
from src.util.utils import encode_uri_component
|
from src.util.utils import encode_uri_component
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +29,6 @@ class ContentManager(ModelManager):
|
|||||||
"type CHAR(30)",
|
"type CHAR(30)",
|
||||||
"location TEXT",
|
"location TEXT",
|
||||||
"duration FLOAT",
|
"duration FLOAT",
|
||||||
"metadata TEXT",
|
|
||||||
"folder_id INTEGER",
|
"folder_id INTEGER",
|
||||||
"created_by CHAR(255)",
|
"created_by CHAR(255)",
|
||||||
"updated_by CHAR(255)",
|
"updated_by CHAR(255)",
|
||||||
@ -43,12 +40,6 @@ class ContentManager(ModelManager):
|
|||||||
super().__init__(lang_manager, database_manager, user_manager, variable_manager)
|
super().__init__(lang_manager, database_manager, user_manager, variable_manager)
|
||||||
self._config_manager = config_manager
|
self._config_manager = config_manager
|
||||||
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
|
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:
|
def hydrate_object(self, raw_content: dict, id: int = None) -> Content:
|
||||||
if id:
|
if id:
|
||||||
@ -82,12 +73,10 @@ class ContentManager(ModelManager):
|
|||||||
def get_all(self, sort: Optional[str] = 'created_at', ascending=False) -> List[Content]:
|
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))
|
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 = {}
|
index = {}
|
||||||
|
|
||||||
items = self.get_by(query) if query else self.get_contents()
|
for item in self.get_contents():
|
||||||
|
|
||||||
for item in items:
|
|
||||||
id = getattr(item, attribute)
|
id = getattr(item, attribute)
|
||||||
if multiple:
|
if multiple:
|
||||||
if id not in index:
|
if id not in index:
|
||||||
@ -143,15 +132,14 @@ class ContentManager(ModelManager):
|
|||||||
def post_delete(self, content_id: str) -> str:
|
def post_delete(self, content_id: str) -> str:
|
||||||
return content_id
|
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)
|
content = self.get(id)
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return
|
return
|
||||||
|
|
||||||
form = {
|
form = {
|
||||||
"name": name if isinstance(name, str) else content.name,
|
"name": name,
|
||||||
"metadata": metadata if isinstance(metadata, str) else content.metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if location is not None and location:
|
if location is not None and location:
|
||||||
@ -210,29 +198,16 @@ class ContentManager(ModelManager):
|
|||||||
object_path = os.path.join(upload_dir, object_name)
|
object_path = os.path.join(upload_dir, object_name)
|
||||||
object.save(object_path)
|
object.save(object_path)
|
||||||
content.location = object_path
|
content.location = object_path
|
||||||
self.set_metadata(content)
|
|
||||||
|
if type == ContentType.VIDEO:
|
||||||
|
content.duration = mp4_duration_with_ffprobe(content.location)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
content.location = ContentType.get_initial_location(content.type, location)
|
content.location = location if location else ''
|
||||||
|
|
||||||
self.add_form(content)
|
self.add_form(content)
|
||||||
return self.get_one_by(query="uuid = '{}'".format(content.uuid))
|
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:
|
def delete(self, id: int) -> None:
|
||||||
content = self.get(id)
|
content = self.get(id)
|
||||||
|
|
||||||
@ -262,16 +237,6 @@ class ContentManager(ModelManager):
|
|||||||
|
|
||||||
if content.type == ContentType.YOUTUBE:
|
if content.type == ContentType.YOUTUBE:
|
||||||
location = content.location
|
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('/')
|
|
||||||
)
|
|
||||||
elif content.has_file() or content.type == ContentType.EXTERNAL_STORAGE:
|
elif content.has_file() or content.type == ContentType.EXTERNAL_STORAGE:
|
||||||
location = "{}/{}".format(
|
location = "{}/{}".format(
|
||||||
var_external_url if len(var_external_url) > 0 else "",
|
var_external_url if len(var_external_url) > 0 else "",
|
||||||
@ -283,13 +248,6 @@ class ContentManager(ModelManager):
|
|||||||
).strip('/')
|
).strip('/')
|
||||||
)
|
)
|
||||||
elif content.type == ContentType.URL:
|
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
|
return location
|
||||||
|
|
||||||
def refresh_all_metadata(self):
|
|
||||||
for content in self.get_all():
|
|
||||||
self.update_form(
|
|
||||||
id=content.id,
|
|
||||||
metadata=self.set_metadata(content)
|
|
||||||
)
|
|
||||||
@ -218,14 +218,13 @@ class UserManager:
|
|||||||
user_id = self.get_logged_user("id")
|
user_id = self.get_logged_user("id")
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
if user_id:
|
if 'created_by' not in object or not object['created_by']:
|
||||||
if 'created_by' not in object or not object['created_by']:
|
object["created_by"] = user_id
|
||||||
object["created_by"] = user_id
|
edits['created_by'] = object['created_by']
|
||||||
edits['created_by'] = object['created_by']
|
|
||||||
|
|
||||||
if 'updated_by' not in object or not object['updated_by']:
|
if 'updated_by' not in object or not object['updated_by']:
|
||||||
object["updated_by"] = user_id
|
object["updated_by"] = user_id
|
||||||
edits['updated_by'] = object['updated_by']
|
edits['updated_by'] = object['updated_by']
|
||||||
|
|
||||||
if 'created_at' not in object or not object['created_at']:
|
if 'created_at' not in object or not object['created_at']:
|
||||||
object["created_at"] = now
|
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_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": "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_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:
|
for default_var in default_vars:
|
||||||
|
|||||||
@ -4,17 +4,15 @@ import uuid
|
|||||||
|
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from src.model.enum.ContentType import ContentType, ContentInputType
|
from src.model.enum.ContentType import ContentType, ContentInputType
|
||||||
from src.model.enum.ContentMetadata import ContentMetadata
|
|
||||||
from src.util.utils import str_to_enum
|
from src.util.utils import str_to_enum
|
||||||
|
|
||||||
|
|
||||||
class Content:
|
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._uuid = uuid if uuid else self.generate_and_set_uuid()
|
||||||
self._id = id if id else None
|
self._id = id if id else None
|
||||||
self._location = location
|
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._type = str_to_enum(type, ContentType) if isinstance(type, str) else type
|
||||||
self._name = name
|
self._name = name
|
||||||
self._folder_id = folder_id
|
self._folder_id = folder_id
|
||||||
@ -41,14 +39,6 @@ class Content:
|
|||||||
def uuid(self, value: str):
|
def uuid(self, value: str):
|
||||||
self._uuid = value
|
self._uuid = value
|
||||||
|
|
||||||
@property
|
|
||||||
def metadata(self) -> str:
|
|
||||||
return self._metadata
|
|
||||||
|
|
||||||
@metadata.setter
|
|
||||||
def metadata(self, value: str):
|
|
||||||
self._metadata = value
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location(self) -> str:
|
def location(self) -> str:
|
||||||
return self._location
|
return self._location
|
||||||
@ -134,7 +124,6 @@ class Content:
|
|||||||
f"updated_at='{self.updated_at}',\n" \
|
f"updated_at='{self.updated_at}',\n" \
|
||||||
f"folder_id='{self.folder_id}',\n" \
|
f"folder_id='{self.folder_id}',\n" \
|
||||||
f"duration='{self.duration}',\n" \
|
f"duration='{self.duration}',\n" \
|
||||||
f"metadata='{self.metadata}',\n" \
|
|
||||||
f")"
|
f")"
|
||||||
|
|
||||||
def to_json(self, edits: dict = {}) -> str:
|
def to_json(self, edits: dict = {}) -> str:
|
||||||
@ -158,7 +147,6 @@ class Content:
|
|||||||
"updated_at": self.updated_at,
|
"updated_at": self.updated_at,
|
||||||
"folder_id": self.folder_id,
|
"folder_id": self.folder_id,
|
||||||
"duration": self.duration,
|
"duration": self.duration,
|
||||||
"metadata": self.metadata,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if with_virtual:
|
if with_virtual:
|
||||||
@ -177,32 +165,3 @@ class Content:
|
|||||||
|
|
||||||
def is_editable(self) -> bool:
|
def is_editable(self) -> bool:
|
||||||
return ContentInputType.is_editable(self.get_input_type())
|
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
|
import mimetypes
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@ -11,9 +10,7 @@ class ContentInputType(Enum):
|
|||||||
|
|
||||||
UPLOAD = 'upload'
|
UPLOAD = 'upload'
|
||||||
TEXT = 'text'
|
TEXT = 'text'
|
||||||
HIDDEN = 'hidden'
|
|
||||||
STORAGE = 'storage'
|
STORAGE = 'storage'
|
||||||
COMPOSITION = 'composition'
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_editable(value: Enum) -> bool:
|
def is_editable(value: Enum) -> bool:
|
||||||
@ -23,8 +20,6 @@ class ContentInputType(Enum):
|
|||||||
return True
|
return True
|
||||||
elif value == ContentInputType.STORAGE:
|
elif value == ContentInputType.STORAGE:
|
||||||
return True
|
return True
|
||||||
elif value == ContentInputType.COMPOSITION:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class ContentType(Enum):
|
class ContentType(Enum):
|
||||||
@ -34,8 +29,6 @@ class ContentType(Enum):
|
|||||||
YOUTUBE = 'youtube'
|
YOUTUBE = 'youtube'
|
||||||
VIDEO = 'video'
|
VIDEO = 'video'
|
||||||
EXTERNAL_STORAGE = 'external_storage'
|
EXTERNAL_STORAGE = 'external_storage'
|
||||||
COMPOSITION = 'composition'
|
|
||||||
TEXT = 'text'
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def guess_content_type_file(filename: str):
|
def guess_content_type_file(filename: str):
|
||||||
@ -68,10 +61,6 @@ class ContentType(Enum):
|
|||||||
return ContentInputType.TEXT
|
return ContentInputType.TEXT
|
||||||
elif value == ContentType.EXTERNAL_STORAGE:
|
elif value == ContentType.EXTERNAL_STORAGE:
|
||||||
return ContentInputType.STORAGE
|
return ContentInputType.STORAGE
|
||||||
elif value == ContentType.COMPOSITION:
|
|
||||||
return ContentInputType.COMPOSITION
|
|
||||||
elif value == ContentType.TEXT:
|
|
||||||
return ContentInputType.TEXT
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_fa_icon(value: Union[Enum, str]) -> str:
|
def get_fa_icon(value: Union[Enum, str]) -> str:
|
||||||
@ -88,10 +77,6 @@ class ContentType(Enum):
|
|||||||
return 'fa-link'
|
return 'fa-link'
|
||||||
elif value == ContentType.EXTERNAL_STORAGE:
|
elif value == ContentType.EXTERNAL_STORAGE:
|
||||||
return 'fa-brands fa-usb'
|
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'
|
return 'fa-file'
|
||||||
|
|
||||||
@ -110,39 +95,5 @@ class ContentType(Enum):
|
|||||||
return 'danger'
|
return 'danger'
|
||||||
elif value == ContentType.EXTERNAL_STORAGE:
|
elif value == ContentType.EXTERNAL_STORAGE:
|
||||||
return 'other'
|
return 'other'
|
||||||
elif value == ContentType.COMPOSITION:
|
|
||||||
return 'purple'
|
|
||||||
elif value == ContentType.TEXT:
|
|
||||||
return 'gscaleF'
|
|
||||||
|
|
||||||
return 'neutral'
|
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 os
|
||||||
import platform
|
import platform
|
||||||
|
import psutil
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from src.util.utils import get_working_directory
|
from src.util.utils import get_working_directory
|
||||||
|
|||||||
@ -39,9 +39,9 @@ class TemplateRenderer:
|
|||||||
AUTH_ENABLED=self._model_store.variable().map().get('auth_enabled').as_bool(),
|
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_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(),
|
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_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(),
|
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_created=self._model_store.user().track_user_created,
|
||||||
track_updated=self._model_store.user().track_user_updated,
|
track_updated=self._model_store.user().track_user_updated,
|
||||||
PORT=self._model_store.config().map().get('port'),
|
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_datetime_moment=is_cron_in_datetime_moment,
|
||||||
is_cron_in_week_moment=is_cron_in_week_moment,
|
is_cron_in_week_moment=is_cron_in_week_moment,
|
||||||
json_dumps=json.dumps,
|
json_dumps=json.dumps,
|
||||||
json_loads=json.loads,
|
|
||||||
merge_dicts=merge_dicts,
|
merge_dicts=merge_dicts,
|
||||||
dictsort=dictsort,
|
dictsort=dictsort,
|
||||||
truncate=truncate,
|
truncate=truncate,
|
||||||
|
|||||||
@ -50,6 +50,7 @@ class WebServer:
|
|||||||
host=self._model_store.config().map().get('bind'),
|
host=self._model_store.config().map().get('bind'),
|
||||||
port=self._model_store.config().map().get('port'),
|
port=self._model_store.config().map().get('port'),
|
||||||
threads=100,
|
threads=100,
|
||||||
|
max_request_body_size=self.get_max_upload_size(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def reload(self) -> None:
|
def reload(self) -> None:
|
||||||
@ -57,7 +58,6 @@ class WebServer:
|
|||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
self._setup_flask_app()
|
self._setup_flask_app()
|
||||||
self._setup_flask_headers()
|
|
||||||
self._setup_web_globals()
|
self._setup_web_globals()
|
||||||
self._setup_web_errors()
|
self._setup_web_errors()
|
||||||
self._setup_web_controllers()
|
self._setup_web_controllers()
|
||||||
@ -160,19 +160,6 @@ class WebServer:
|
|||||||
def inject_global_vars() -> dict:
|
def inject_global_vars() -> dict:
|
||||||
return self._template_renderer.get_view_globals()
|
return self._template_renderer.get_view_globals()
|
||||||
|
|
||||||
def _setup_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 _setup_web_errors(self) -> None:
|
||||||
def handle_error(error):
|
def handle_error(error):
|
||||||
if request.headers.get('Content-Type') == 'application/json' or request.headers.get('Accept') == 'application/json':
|
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 make_response(response, error.code)
|
||||||
|
|
||||||
return self._template_renderer.render_view(
|
if error.code == 404:
|
||||||
'@core/http-error.html',
|
return send_from_directory(self.get_template_dir(), 'core/error404.html'), 404
|
||||||
error_code=error.code,
|
|
||||||
error_message=error.description
|
return error
|
||||||
)
|
|
||||||
|
|
||||||
self._app.register_error_handler(400, handle_error)
|
self._app.register_error_handler(400, handle_error)
|
||||||
self._app.register_error_handler(404, handle_error)
|
self._app.register_error_handler(404, handle_error)
|
||||||
self._app.register_error_handler(409, 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)
|
self._app.register_error_handler(HttpClientException, handle_error)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
try:
|
|
||||||
import psutil
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import psutil
|
||||||
import platform
|
import platform
|
||||||
import logging
|
import logging
|
||||||
import os
|
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")
|
|
||||||
@ -5,7 +5,7 @@ import json
|
|||||||
from pymediainfo import MediaInfo
|
from pymediainfo import MediaInfo
|
||||||
|
|
||||||
|
|
||||||
def get_video_metadata(filename):
|
def mp4_duration_with_ffprobe(filename):
|
||||||
try:
|
try:
|
||||||
result = subprocess.check_output(f'ffprobe -v quiet -show_streams -select_streams v:0 -of json "{filename}"', shell=True).decode()
|
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]
|
fields = json.loads(result)['streams'][0]
|
||||||
@ -16,10 +16,7 @@ def get_video_metadata(filename):
|
|||||||
elif 'duration' in fields:
|
elif 'duration' in fields:
|
||||||
duration = round(float(fields['duration']), 2)
|
duration = round(float(fields['duration']), 2)
|
||||||
|
|
||||||
width = fields.get('width', 0)
|
return duration
|
||||||
height = fields.get('height', 0)
|
|
||||||
|
|
||||||
return width, height, duration
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
logging.warn("ffprobe not found or an error occurred. Using pymediainfo instead.")
|
logging.warn("ffprobe not found or an error occurred. Using pymediainfo instead.")
|
||||||
|
|
||||||
@ -28,13 +25,11 @@ def get_video_metadata(filename):
|
|||||||
for track in media_info.tracks:
|
for track in media_info.tracks:
|
||||||
if track.track_type == "Video":
|
if track.track_type == "Video":
|
||||||
duration = round(track.duration / 1000, 2) if track.duration else None
|
duration = round(track.duration / 1000, 2) if track.duration else None
|
||||||
width = track.width
|
|
||||||
height = track.height
|
|
||||||
|
|
||||||
return width, height, duration
|
return duration
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.warn("Fail to get video metadata from pymediainfo.")
|
logging.warn("Fail to get video metadata from pymediainfo.")
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logging.warn("Fail to get video metadata from ffprobe.")
|
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
|
# Disable screensaver and DPMS
|
||||||
xset s off
|
xset s off
|
||||||
xset -dpms
|
xset -dpms
|
||||||
@ -13,15 +7,10 @@ xset s noblank
|
|||||||
unclutter -display :0 -noevents -grab &
|
unclutter -display :0 -noevents -grab &
|
||||||
|
|
||||||
# Modify Chromium preferences to avoid restore messages
|
# Modify Chromium preferences to avoid restore messages
|
||||||
mkdir -p /tmp/obscreen/chromium/Default 2>/dev/null
|
mkdir -p /home/pi/.config/chromium/Default 2>/dev/null
|
||||||
touch /tmp/obscreen/chromium/Default/Preferences
|
touch /home/pi/.config/chromium/Default/Preferences
|
||||||
sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' /tmp/obscreen/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}')
|
RESOLUTION=$(DISPLAY=:0 xrandr | grep '*' | awk '{print $1}')
|
||||||
WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1)
|
WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1)
|
||||||
HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2)
|
HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2)
|
||||||
@ -40,9 +29,9 @@ chromium-browser \
|
|||||||
--noerrdialogs \
|
--noerrdialogs \
|
||||||
--kiosk \
|
--kiosk \
|
||||||
--incognito \
|
--incognito \
|
||||||
--user-data-dir=/tmp/obscreen/chromium \
|
--user-data-dir=/home/pi/.config/chromium \
|
||||||
--no-sandbox \
|
--no-sandbox \
|
||||||
--window-position=0,0 \
|
--window-position=0,0 \
|
||||||
--window-size=${WIDTH},${HEIGHT} \
|
--window-size=${WIDTH},${HEIGHT} \
|
||||||
--display=:0 \
|
--display=:0 \
|
||||||
${STUDIO_URL}
|
http://localhost:5000
|
||||||
|
|||||||
@ -96,7 +96,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
|
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
|
# 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
|
# Reload systemd, enable and start the service
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
@ -108,7 +108,7 @@ systemctl set-default graphical.target
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
mkdir -p "$WORKING_DIR/obscreen/var/run"
|
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#chromium-browser#$CHROMIUM#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"
|
chmod +x "$WORKING_DIR/obscreen/var/run/play"
|
||||||
chown -R $OWNER:$OWNER "$WORKING_DIR/obscreen"
|
chown -R $OWNER:$OWNER "$WORKING_DIR/obscreen"
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ apt-get install -y git build-essential gcc python3-dev python3-pip python3-venv
|
|||||||
|
|
||||||
# Get files
|
# Get files
|
||||||
cd $WORKING_DIR
|
cd $WORKING_DIR
|
||||||
git clone https://github.com/csmith1865/obscreen.git
|
git clone https://github.com/jr-k/obscreen.git
|
||||||
cd obscreen
|
cd obscreen
|
||||||
|
|
||||||
# Install application dependencies
|
# Install application dependencies
|
||||||
@ -44,7 +44,7 @@ chown -R $OWNER:$OWNER ./
|
|||||||
# Automount script for external storage
|
# 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
|
udevadm control --reload-rules
|
||||||
systemctl restart udev
|
systemctl restart udev
|
||||||
udevadm trigger
|
udevadm trigger
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
2.4.4
|
2.5.0
|
||||||
@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
{% block body_class %}view-auth-user-list{% endblock %}
|
{% block body_class %}view-auth-user-list{% endblock %}
|
||||||
|
|
||||||
{% block top_page %}
|
{% block page %}
|
||||||
|
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_START) }}
|
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_START) }}
|
||||||
@ -25,9 +26,12 @@
|
|||||||
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_END) }}
|
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_END) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main_page %}
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{ l[error] }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="bottom-content">
|
<div class="bottom-content">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
|||||||
@ -14,7 +14,14 @@
|
|||||||
|
|
||||||
{% block body_class %}view-login{% endblock %}
|
{% 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="login-content">
|
||||||
<div class="form-holder">
|
<div class="form-holder">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@ -180,14 +180,14 @@
|
|||||||
|
|
||||||
<div class="context-divider"></div>
|
<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">
|
<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>
|
<i class="fa fa-display main"></i>
|
||||||
<sub><i class="fa fa-refresh"></i></sub>
|
<sub><i class="fa fa-refresh"></i></sub>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if fully_authenticated_view and AUTH_ENABLED %}
|
{% if fully_authenticated_view %}
|
||||||
<div class="context-divider"></div>
|
<div class="context-divider"></div>
|
||||||
<div class="context-user">
|
<div class="context-user">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
@ -213,41 +213,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="main-container">
|
<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 %}
|
{% block page %}{% endblock %}
|
||||||
{% 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 %}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -256,7 +229,6 @@
|
|||||||
<script>
|
<script>
|
||||||
var secret_key = '{{ SECRET_KEY }}';
|
var secret_key = '{{ SECRET_KEY }}';
|
||||||
var l = {
|
var l = {
|
||||||
'js_common_empty': '{{ l.common_empty }}',
|
|
||||||
'js_common_are_you_sure': '{{ l.common_are_you_sure }}',
|
'js_common_are_you_sure': '{{ l.common_are_you_sure }}',
|
||||||
'js_playlist_delete_confirmation': '{{ l.js_playlist_delete_confirmation }}',
|
'js_playlist_delete_confirmation': '{{ l.js_playlist_delete_confirmation }}',
|
||||||
'js_slideshow_slide_delete_confirmation': '{{ l.js_slideshow_slide_delete_confirmation }}',
|
'js_slideshow_slide_delete_confirmation': '{{ l.js_slideshow_slide_delete_confirmation }}',
|
||||||
@ -271,7 +243,6 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery.min.js"></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/utils.js"></script>
|
||||||
<script src="{{ STATIC_PREFIX }}js/global.js"></script>
|
<script src="{{ STATIC_PREFIX }}js/global.js"></script>
|
||||||
{{ HOOK(H_ROOT_JAVASCRIPT) }}
|
{{ HOOK(H_ROOT_JAVASCRIPT) }}
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
|
|
||||||
{% block body_class %}view-logs-list{% endblock %}
|
{% block body_class %}view-logs-list{% endblock %}
|
||||||
|
|
||||||
{% block main_page %}
|
{% block page %}
|
||||||
|
|
||||||
<div class="bottom-content">
|
<div class="bottom-content">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|||||||
@ -11,7 +11,20 @@
|
|||||||
|
|
||||||
{% block body_class %}view-plugins-list{% endblock %}
|
{% 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="bottom-content">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|||||||
@ -11,7 +11,20 @@
|
|||||||
|
|
||||||
{% block body_class %}view-settings-list{% endblock %}
|
{% 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="bottom-content">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
@ -29,4 +42,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
{% block body_class %}view-sysinfo-list{% endblock %}
|
{% block body_class %}view-sysinfo-list{% endblock %}
|
||||||
|
|
||||||
{% block top_page %}
|
{% block page %}
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<div class="top-actions align-right">
|
<div class="top-actions align-right">
|
||||||
{{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_START) }}
|
{{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_START) }}
|
||||||
@ -24,9 +24,7 @@
|
|||||||
{{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_END) }}
|
{{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_END) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main_page %}
|
|
||||||
<div class="bottom-content">
|
<div class="bottom-content">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -6,10 +6,13 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block add_css %}
|
{% 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) }}
|
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block add_js %}
|
{% 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>
|
<script src="{{ STATIC_PREFIX }}js/fleet/node-players.js"></script>
|
||||||
|
|
||||||
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
||||||
@ -17,15 +20,20 @@
|
|||||||
|
|
||||||
{% block body_class %}view-node-player-edit edit-page{% endblock %}
|
{% block body_class %}view-node-player-edit edit-page{% endblock %}
|
||||||
|
|
||||||
{% block top_page %}
|
{% block page %}
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<h1>
|
<h1>
|
||||||
{{ l.fleet_node_player_form_edit_title }}
|
{{ l.fleet_node_player_form_edit_title }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</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="bottom-content">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="inner dirview">
|
<div class="inner dirview">
|
||||||
|
|||||||
@ -20,7 +20,9 @@
|
|||||||
|
|
||||||
{% block body_class %}view-node-player-list{% endblock %}
|
{% 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-content">
|
||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_START) }}
|
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_START) }}
|
||||||
@ -59,10 +61,20 @@
|
|||||||
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_END) }}
|
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_END) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main_page %}
|
{% if request.args.get('folder_not_empty_error') %}
|
||||||
{% set explr_limit_chars = 35 %}
|
<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="bottom-content">
|
||||||
<div class="page-panel left-panel explr-explorer">
|
<div class="page-panel left-panel explr-explorer">
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
<h2>
|
<h2>
|
||||||
{{ l.common_pick_element }}
|
{{ l.common_pick_element }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{% with use_href=False %}
|
{% with use_href=False %}
|
||||||
{% include 'fleet/node-players/component/explr-sidebar.jinja.html' %}
|
{% include 'fleet/node-players/component/explr-sidebar.jinja.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn btn-naked picker-close">
|
<button type="button" class="btn btn-naked picker-close">
|
||||||
<i class="fa fa-close icon-left"></i>{{ l.common_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 body_class %}view-player-group-list{% endblock %}
|
||||||
|
|
||||||
{% block top_page %}
|
{% block page %}
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
{{ HOOK(H_FLEET_NODE_PLAYER_GROUP_TOOLBAR_ACTIONS_START) }}
|
{{ 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
|
('<a href="javascript:void(0);" class="item-add node-player-group-add">'~l.fleet_node_player_group_button_add~'</a>')|safe
|
||||||
) }}
|
) }}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main_page %}
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{ l[error] }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="bottom-content">
|
<div class="bottom-content">
|
||||||
<div class="page-panel left-panel">
|
<div class="page-panel left-panel">
|
||||||
{% with node_player_groups=node_player_groups %}
|
{% 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>
|
|
||||||
@ -5,37 +5,17 @@
|
|||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<meta name="google" content="notranslate">
|
<meta name="google" content="notranslate">
|
||||||
<link rel="shortcut icon" href="{{ STATIC_PREFIX }}/favicon.ico">
|
<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 %}
|
{% if slide_animation_enabled %}
|
||||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/animate.min.css" />
|
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/animate.min.css" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<style>
|
<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; }
|
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: {{ 'transparent' if transparent else 'black' }}; }
|
.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 { 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 iframe { background: white; }
|
||||||
.slide img, .slide video { {% if force_cover %}width: 100%;{% endif %} height: 100%; }
|
.slide img, .slide video { 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 video { width: 100%; 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>
|
</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/utils.js"></script>
|
||||||
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/lib/is-cron-now.js"></script>
|
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/lib/is-cron-now.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@ -67,7 +47,6 @@
|
|||||||
// Frontend config
|
// Frontend config
|
||||||
const syncWithTime = items['time_sync'];
|
const syncWithTime = items['time_sync'];
|
||||||
const previewMode = items['preview_mode'];
|
const previewMode = items['preview_mode'];
|
||||||
const disableAutoplay = {{ 'false' if request.args.get('autoplay') == '1' else 'previewMode' }};
|
|
||||||
const tickRefreshResolutionMs = 100;
|
const tickRefreshResolutionMs = 100;
|
||||||
|
|
||||||
// Frontend flag updates
|
// Frontend flag updates
|
||||||
@ -360,18 +339,12 @@
|
|||||||
case 'picture':
|
case 'picture':
|
||||||
loadPicture(element, callbackReady, item);
|
loadPicture(element, callbackReady, item);
|
||||||
break;
|
break;
|
||||||
case 'text':
|
|
||||||
loadText(element, callbackReady, item);
|
|
||||||
break;
|
|
||||||
case 'video':
|
case 'video':
|
||||||
loadVideo(element, callbackReady, item);
|
loadVideo(element, callbackReady, item);
|
||||||
break;
|
break;
|
||||||
case 'youtube':
|
case 'youtube':
|
||||||
loadYoutube(element, callbackReady, item);
|
loadYoutube(element, callbackReady, item);
|
||||||
break;
|
break;
|
||||||
case 'composition':
|
|
||||||
loadComposition(element, callbackReady, item);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
loadUrl(element, callbackReady, item);
|
loadUrl(element, callbackReady, item);
|
||||||
break;
|
break;
|
||||||
@ -388,65 +361,6 @@
|
|||||||
callbackReady(function() {});
|
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) {
|
const loadYoutube = function(element, callbackReady, item) {
|
||||||
element.innerHTML = `youtube`;
|
element.innerHTML = `youtube`;
|
||||||
callbackReady(function() {});
|
callbackReady(function() {});
|
||||||
@ -461,7 +375,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (element.innerHTML === 'youtube') {
|
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>`;
|
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 +383,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadVideo = function(element, callbackReady, item) {
|
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');
|
const video = element.querySelector('video');
|
||||||
callbackReady(function() {});
|
callbackReady(function() {});
|
||||||
|
|
||||||
@ -493,7 +407,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (element.innerHTML.match('<video>')) {
|
if (element.innerHTML.match('<video>')) {
|
||||||
if (!disableAutoplay) {
|
if (!previewMode) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
video.play();
|
video.play();
|
||||||
pausableContent = video;
|
pausableContent = video;
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
{% block body_class %}view-playlist-list{% endblock %}
|
{% block body_class %}view-playlist-list{% endblock %}
|
||||||
|
|
||||||
{% block top_page %}
|
{% block page %}
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
{{ HOOK(H_PLAYLIST_TOOLBAR_ACTIONS_START) }}
|
{{ HOOK(H_PLAYLIST_TOOLBAR_ACTIONS_START) }}
|
||||||
@ -119,9 +119,13 @@
|
|||||||
('<a href="javascript:void(0);" class="item-add playlist-add">'~l.playlist_button_add~'</a>')|safe
|
('<a href="javascript:void(0);" class="item-add playlist-add">'~l.playlist_button_add~'</a>')|safe
|
||||||
) }}
|
) }}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main_page %}
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{ l[error] }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="bottom-content">
|
<div class="bottom-content">
|
||||||
<div class="page-panel left-panel">
|
<div class="page-panel left-panel">
|
||||||
{% with playlists=playlists %}
|
{% with playlists=playlists %}
|
||||||
|
|||||||
@ -14,17 +14,17 @@
|
|||||||
{{ render_folder(child) }}
|
{{ render_folder(child) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for content in content_children %}
|
{% 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 icon = enum_content_type.get_fa_icon(content.type) %}
|
||||||
{% set color = enum_content_type.get_color_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>
|
<i class="fa {{ icon }} {{ color }}"></i>
|
||||||
{% if slides|length > 0 %}
|
{% if slides|length > 0 %}
|
||||||
<sub>
|
<sub>
|
||||||
<i class="fa fa-play"></i>
|
<i class="fa fa-play"></i>
|
||||||
</sub>
|
</sub>
|
||||||
{% endif %}
|
{% 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 }}">
|
class="{{ 'explr-pick-element' if not use_href }}">
|
||||||
{{ content.name }}
|
{{ content.name }}
|
||||||
</a>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block add_css %}
|
{% 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) }}
|
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block add_js %}
|
{% block add_js %}
|
||||||
|
<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/slideshow/contents.js"></script>
|
||||||
|
|
||||||
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
||||||
@ -17,24 +20,20 @@
|
|||||||
|
|
||||||
{% block body_class %}view-content-edit edit-page{% endblock %}
|
{% block body_class %}view-content-edit edit-page{% endblock %}
|
||||||
|
|
||||||
{% block top_page %}
|
{% block page %}
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<h1>
|
<h1>
|
||||||
{{ l.slideshow_content_form_edit_title }}
|
{{ l.slideshow_content_form_edit_title }}
|
||||||
</h1>
|
</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>
|
</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="bottom-content">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="inner dirview">
|
<div class="inner dirview">
|
||||||
@ -81,7 +80,7 @@
|
|||||||
{% if content.type == enum_content_type.EXTERNAL_STORAGE %}
|
{% if content.type == enum_content_type.EXTERNAL_STORAGE %}
|
||||||
<input type="text" class="disabled" disabled value="{{ external_storage_mountpoint }}/" />
|
<input type="text" class="disabled" disabled value="{{ external_storage_mountpoint }}/" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set location = content.location %}
|
{% set location = content.location %}
|
||||||
{% if content.type == enum_content_type.YOUTUBE %}
|
{% if content.type == enum_content_type.YOUTUBE %}
|
||||||
{% set location = 'https://www.youtube.com/watch?v=' ~ content.location %}
|
{% set location = 'https://www.youtube.com/watch?v=' ~ content.location %}
|
||||||
@ -90,24 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if content.type == enum_content_type.VIDEO or content.type == enum_content_type.PICTURE %}
|
<div class="actions actions-left">
|
||||||
<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">
|
|
||||||
<button type="submit" class="btn btn-info">
|
<button type="submit" class="btn btn-info">
|
||||||
<i class="fa fa-save icon-left"></i>
|
<i class="fa fa-save icon-left"></i>
|
||||||
{{ l.common_save }}
|
{{ l.common_save }}
|
||||||
@ -125,6 +107,14 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="page-panel right-panel">
|
<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">
|
<div class="iframe-wrapper">
|
||||||
<iframe src="{{ url_for('player', preview_content_id=content.id) }}"></iframe>
|
<iframe src="{{ url_for('player', preview_content_id=content.id) }}"></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,14 +22,16 @@
|
|||||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
|
<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-fileupload.js"></script>
|
||||||
<script src="{{ STATIC_PREFIX }}js/lib/jquery-multidraggable.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) }}
|
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body_class %}view-content-list{% 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-content">
|
||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
{{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }}
|
{{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }}
|
||||||
@ -83,11 +85,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-upload alert-danger hidden"></div>
|
{% if request.args.get('folder_not_empty_error') %}
|
||||||
{% endblock %}
|
<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="bottom-content">
|
||||||
<div class="page-panel left-panel explr-explorer">
|
<div class="page-panel left-panel explr-explorer">
|
||||||
{% with use_href=True %}
|
{% with use_href=True %}
|
||||||
|
|||||||
@ -52,28 +52,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="from-group-condition hidden">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
<button type="button" class="btn btn-naked content-explr-picker">
|
<button type="button" class="btn btn-naked content-explr-picker">
|
||||||
<i class="fa fa-crosshairs"></i>
|
<i class="fa fa-crosshairs"></i>
|
||||||
</button>
|
</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>
|
<i class="fa-solid fa-up-right-from-square"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -88,23 +88,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="horizontal">
|
<div class="form-group form-group-horizontal slide-delegate-duration-group">
|
||||||
<div class="form-group slide-delegate-duration-group">
|
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
|
||||||
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
|
<div class="widget">
|
||||||
<div class="widget">
|
<div class="toggle">
|
||||||
<div class="toggle">
|
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
|
||||||
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
|
<label for="{{ tclass }}-delegate-duration"></label>
|
||||||
<label for="{{ tclass }}-delegate-duration"></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group slide-duration-group">
|
<div class="form-group slide-duration-group">
|
||||||
<label for="{{ tclass }}-duration">{{ l.slideshow_slide_form_label_duration }}</label>
|
<label for="{{ tclass }}-duration">{{ l.slideshow_slide_form_label_duration }}</label>
|
||||||
<div class="widget widget-unit">
|
<div class="widget widget-unit">
|
||||||
<input type="text" name="duration" id="{{ tclass }}-duration" required="required" value="3" min="0" class="numeric-input" />
|
<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>
|
<span class="unit">{{ l.slideshow_slide_form_label_duration_unit }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
<button type="button" class="btn btn-naked content-explr-picker">
|
<button type="button" class="btn btn-naked content-explr-picker">
|
||||||
<i class="fa fa-crosshairs"></i>
|
<i class="fa fa-crosshairs"></i>
|
||||||
</button>
|
</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>
|
<i class="fa-solid fa-up-right-from-square"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -86,22 +86,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="horizontal">
|
<div class="form-group form-group-horizontal slide-delegate-duration-group">
|
||||||
<div class="form-group slide-delegate-duration-group">
|
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
|
||||||
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
|
<div class="widget">
|
||||||
<div class="widget">
|
<div class="toggle">
|
||||||
<div class="toggle">
|
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
|
||||||
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
|
<label for="{{ tclass }}-delegate-duration"></label>
|
||||||
<label for="{{ tclass }}-delegate-duration"></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group slide-duration-group">
|
</div>
|
||||||
<label for="{{ tclass }}-duration">{{ l.slideshow_slide_form_label_duration }}</label>
|
|
||||||
<div class="widget widget-unit">
|
<div class="form-group slide-duration-group">
|
||||||
<input type="text" name="duration" id="{{ tclass }}-duration" required="required" min="0" class="numeric-input" />
|
<label for="{{ tclass }}-duration">{{ l.slideshow_slide_form_label_duration }}</label>
|
||||||
<span class="unit">{{ l.slideshow_slide_form_label_duration_unit }}</span>
|
<div class="widget widget-unit">
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user