Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc7e573b49 | |||
| 151e66e0b3 | |||
| a05c7b2bbd |
@ -1,27 +1,18 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
/data/uploads/*
|
||||
!/data/uploads/.gitkeep
|
||||
/data/db/*
|
||||
data/uploads/*
|
||||
!data/uploads/.gitkeep
|
||||
data/db/*
|
||||
!/data/db/.gitkeep
|
||||
/plugins/user/*
|
||||
!/plugins/user/.gitkeep
|
||||
*.lock
|
||||
__pycache__/
|
||||
*.log
|
||||
/var/run/*
|
||||
!/var/run/.gitkeep
|
||||
*.swp
|
||||
var/run/*
|
||||
!var/run/.gitkeep
|
||||
.env
|
||||
venv/
|
||||
node_modules
|
||||
tmp.py
|
||||
!/plugins/user/Dashboard
|
||||
/data/www/plugins/*
|
||||
!/data/www/plugins/.gitkeep
|
||||
/var/run/storage/*
|
||||
!/var/run/storage/.gitkeep
|
||||
@ -5,7 +5,12 @@ SECRET_KEY=ANY_SECRET_KEY_HERE
|
||||
# Application Server
|
||||
PORT=5000
|
||||
BIND=0.0.0.0
|
||||
EXTERNAL_STORAGE_MOUNTPOINT=%application_dir%/var/run/storage
|
||||
|
||||
|
||||
# HTTP External Storage Server
|
||||
PORT_HTTP_EXTERNAL_STORAGE=5001
|
||||
BIND_HTTP_EXTERNAL_STORAGE=0.0.0.0
|
||||
CHROOT_HTTP_EXTERNAL_STORAGE=%application_dir%/var/run/storage
|
||||
|
||||
# Misc
|
||||
DEMO=false
|
||||
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
||||
github: jr-k
|
||||
custom: https://paypal.me/jierka
|
||||
|
||||
0
.github/actions/common-docker-build/action.yml
vendored
Executable file → Normal file
0
.github/actions/common-docker-build/action.yml
vendored
Executable file → Normal file
41
.github/workflows/build-nightly.yml
vendored
41
.github/workflows/build-nightly.yml
vendored
@ -1,41 +0,0 @@
|
||||
name: Nightly build synced with develop and push docker image
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push-nightly:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
- name: Sync nightly branch
|
||||
run: |
|
||||
git checkout nightly
|
||||
git merge develop --no-edit
|
||||
git push origin nightly --force
|
||||
|
||||
- name: Checkout nightly branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
- name: Call common build workflow
|
||||
uses: ./.github/actions/common-docker-build
|
||||
with:
|
||||
build_tags: csmith1865/obscreen:nightly
|
||||
manifest_tags: type=semver,pattern=nightly
|
||||
flavor: ""
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
28
.github/workflows/build-pr.yml
vendored
28
.github/workflows/build-pr.yml
vendored
@ -1,28 +0,0 @@
|
||||
name: PR build and push docker image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, labeled]
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
- develop
|
||||
- nightly
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.pull_request.labels.*.name, 'build-pr')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Call common build workflow
|
||||
uses: ./.github/actions/common-docker-build
|
||||
with:
|
||||
build_tags: csmith1865/obscreen:pr-${{ github.event.pull_request.number }}
|
||||
manifest_tags: type=semver,pattern=pr
|
||||
flavor: ""
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@ -24,7 +24,7 @@ jobs:
|
||||
with:
|
||||
build_tags: |
|
||||
csmith1865/obscreen:v${{ steps.version.outputs.VERSION }}
|
||||
csmith1865/obscreen:latest
|
||||
csmith1865/obscreen:latest
|
||||
manifest_tags: type=semver,pattern=v${{ steps.version.outputs.VERSION }}
|
||||
flavor: latest=true
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
@ -4,27 +4,22 @@
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
/data/uploads/*
|
||||
!/data/uploads/.gitkeep
|
||||
/data/db/*
|
||||
data/uploads/*
|
||||
!data/uploads/.gitkeep
|
||||
data/db/*
|
||||
!/data/db/.gitkeep
|
||||
/plugins/user/*
|
||||
!/plugins/user/.gitkeep
|
||||
*.lock
|
||||
__pycache__/
|
||||
*.log
|
||||
/var/run/*
|
||||
!/var/run/.gitkeep
|
||||
var/run/*
|
||||
!var/run/.gitkeep
|
||||
*.swp
|
||||
.env
|
||||
venv/
|
||||
node_modules
|
||||
tmp.py
|
||||
!/plugins/user/Dashboard
|
||||
/data/www/plugins/*
|
||||
!/data/www/plugins/.gitkeep
|
||||
/var/run/storage/*
|
||||
!/var/run/storage/.gitkeep
|
||||
*.egg-info
|
||||
/build/
|
||||
/dist/
|
||||
data/www/plugins/*
|
||||
!data/www/plugins/.gitkeep
|
||||
17
Dockerfile
17
Dockerfile
@ -1,24 +1,13 @@
|
||||
FROM python:3.9-slim-bullseye
|
||||
FROM python:3.9.17-alpine3.17
|
||||
|
||||
# Install ffmpeg and other dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libc6-dev \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
ntfs-3g \
|
||||
ffmpeg \
|
||||
build-essential \
|
||||
curl \
|
||||
tar \
|
||||
bash \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache gcc musl-dev sqlite-dev ntfs-3g ffmpeg build-base linux-headers
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
ENTRYPOINT ["python", "/app/obscreen.py"]
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
docs/setup-run-on-rpi.md
|
||||
docs/setup-run-headless.md
|
||||
40
README.md
40
README.md
@ -6,15 +6,10 @@
|
||||
|
||||
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)
|
||||
|
||||
⭐️ You liked it ? Give this repository a star, it's free :)
|
||||
|
||||
---
|
||||
|
||||
## 🕹️ Live Demo
|
||||
|
||||
@ -24,7 +19,7 @@ Demo Server (Location: Roubaix - France): [https://demo.obscreen.io](https://dem
|
||||
|
||||
It is a temporary live demo, all data will be deleted after 30 minutes (~30secs downtime).
|
||||
|
||||
## 🎉 Features
|
||||
## ⭐️ Features
|
||||
- Dead simple chromium webview inside
|
||||
- Fancy graphical user interface
|
||||
- Very few dependencies
|
||||
@ -33,8 +28,8 @@ It is a temporary live demo, all data will be deleted after 30 minutes (~30secs
|
||||
- Playlist management
|
||||
- Authentication management
|
||||
- Plays content from flashdrive in offline mode
|
||||
- Core API & Plugin system to extend capabilities
|
||||
- [Multi Languages](https://git.sumisu.xyz/csmith1865/obscreen/src/branch/master/lang)
|
||||
- Plugin system to extend capabilities
|
||||
- [Multi Languages](https://github.com/jr-k/obscreen/tree/master/lang)
|
||||
- Cast pictures and iframes to Chromecast
|
||||
- No costly monthly pricing plan per screen or whatever, no cloud, no telemetry
|
||||
|
||||
@ -47,19 +42,19 @@ It is a temporary live demo, all data will be deleted after 30 minutes (~30secs
|
||||
|
||||
Light Mode:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-light-mode.png" width="512" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-light-mode.png" width="512" alt="" />
|
||||
|
||||
Content Explorer:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-content-explorer.png" width="512" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-content-explorer.png" width="512" alt="" />
|
||||
|
||||
Settings Page:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-settings.png" width="512" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-settings.png" width="512" alt="" />
|
||||
|
||||
Add Content Modal:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-add-content.png" width="512" alt="" />
|
||||
<img src="https://github.com/jr-k/obscreen/blob/master/docs/screenshot-add-content.png" width="512" alt="" />
|
||||
|
||||
## 🫡 Motivation
|
||||
|
||||
@ -71,30 +66,23 @@ Add Content Modal:
|
||||
|
||||
If you value this project, please think about awarding it a ⭐. Thanks ! 🙏
|
||||
|
||||
## 🗺️ Short-term roadmap
|
||||
- New `Composition` content type: Check out a [video demo here](https://demo.obscreen.io/data/uploads/compositions.mp4)
|
||||
- New `Text` Content Type: Display text with customizable styles, including options for scrolling effects.
|
||||
- New `HTML` Content Type: Display HTML snippets for more powerful text customization, giving you full control over the content.
|
||||
- Fleet Studio Management: Reviving a legacy feature
|
||||
- Remote Player Server: A new way to manage a player from the studio without needing SSH access to player
|
||||
|
||||
## 🛟 Discussion / Need help ?
|
||||
|
||||
### Join our Discord
|
||||
[<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/img/discord.png" width="64">](https://discord.obscreen.io)
|
||||
[<img src="https://github.com/jr-k/obscreen/blob/master/docs/img/discord.png" width="64">](https://discord.obscreen.io)
|
||||
|
||||
### Open an Issue
|
||||
[<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/img/github.png" width="64">](https://git.sumisu.xyz/csmith1865/obscreen/issues/new/choose)
|
||||
[<img src="https://github.com/jr-k/obscreen/blob/master/docs/img/github.png" width="64">](https://github.com/jr-k/obscreen/issues/new/choose)
|
||||
|
||||
### Troubleshoot
|
||||
|
||||
|
||||
<details closed>
|
||||
<summary><h3>Why aren't the videos starting?</h3></summary>
|
||||
<summary><h3>Videos aren't playing why ?</h3></summary>
|
||||
|
||||
This is "normal" behavior. Videos do not play automatically in Chrome because it requires user interaction with the page (a simple click inside the webpage is enough). If you open the console, you'll see the error: [Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first...](https://goo.gl/xX8pDD)
|
||||
|
||||
To resolve this, you need to use the Chrome flag `--autoplay-policy=no-user-gesture-required`. When connecting a Raspberry Pi with Obscreen Player autorun, this issue doesn't occur because the flag is handled automatically for you. You need to enable this flag yourself otherwise.
|
||||
To resolve this, you need to use the Chrome flag --autoplay-policy=no-user-gesture-required. When connecting a Raspberry Pi with Obscreen Player autorun, this issue doesn't occur because the flag is handled automatically for you.You need to enable this flag yourself otherwise.
|
||||
|
||||
---
|
||||
|
||||
@ -112,7 +100,7 @@ Check out the latest beta release here: https://github.com/jr-k/obscreen/release
|
||||
|
||||
### Translations
|
||||
|
||||
If you want to translate Obscreen into your language, please visit [Languages Files](https://git.sumisu.xyz/csmith1865/obscreen/src/branch/master/lang).
|
||||
If you want to translate Obscreen into your language, please visit [Languages Files](https://github.com/jr-k/obscreen/blob/master/lang).
|
||||
|
||||
### Spelling & Grammar
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,8 +1,6 @@
|
||||
jQuery(document).ready(function ($) {
|
||||
const main = function () {
|
||||
$('.user-token-reveal').each(function() {
|
||||
updateTokenReveal($(this), false);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$(document).on('click', '.user-add', function () {
|
||||
@ -19,26 +17,5 @@ jQuery(document).ready(function ($) {
|
||||
$('#user-edit-id').val(user.id);
|
||||
});
|
||||
|
||||
const updateTokenReveal = function($btn, revealState) {
|
||||
const $holder = $btn.parents('.user-item:eq(0)');
|
||||
const $input = $holder.find('.input-token:eq(0)');
|
||||
const $icon = $btn.find('i:eq(0)');
|
||||
const isActive = revealState !== undefined ? !revealState : $icon.hasClass('fa-eye-slash');
|
||||
|
||||
if (isActive) {
|
||||
$icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
$btn.removeClass('btn-neutral').addClass('btn-other');
|
||||
$input.val($input.attr('data-private'));
|
||||
} else {
|
||||
$icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
$btn.removeClass('btn-other').addClass('btn-neutral');
|
||||
$input.val($input.attr('data-public'));
|
||||
}
|
||||
};
|
||||
|
||||
$(document).on('click', '.user-token-reveal', function () {
|
||||
updateTokenReveal($(this));
|
||||
});
|
||||
|
||||
main();
|
||||
});
|
||||
@ -1,74 +0,0 @@
|
||||
var applicationID = '81585E3E';
|
||||
var namespace = 'urn:x-cast:com.jrk.obscreen';
|
||||
var session = null;
|
||||
|
||||
if (!chrome.cast || !chrome.cast.isAvailable) {
|
||||
setTimeout(initializeCastApi, 1000);
|
||||
}
|
||||
|
||||
function initializeCastApi() {
|
||||
var sessionRequest = new chrome.cast.SessionRequest(applicationID);
|
||||
var apiConfig = new chrome.cast.ApiConfig(sessionRequest,
|
||||
sessionListener,
|
||||
receiverListener);
|
||||
|
||||
chrome.cast.initialize(apiConfig, onInitSuccess, onError);
|
||||
}
|
||||
|
||||
function onInitSuccess() {
|
||||
// console.log('onInitSuccess');
|
||||
}
|
||||
|
||||
function onError(message) {
|
||||
console.error('onError: ' + JSON.stringify(message));
|
||||
}
|
||||
|
||||
function onSuccess(message) {
|
||||
// console.log('onSuccess: ' + JSON.stringify(message));
|
||||
}
|
||||
|
||||
// function onStopAppSuccess() {
|
||||
// console.log('onStopAppSuccess');
|
||||
// }
|
||||
|
||||
function sessionListener(e) {
|
||||
console.log('New session ID: ' + e.sessionId);
|
||||
session = e;
|
||||
session.addUpdateListener(sessionUpdateListener);
|
||||
}
|
||||
|
||||
function sessionUpdateListener(isAlive) {
|
||||
console.log((isAlive ? 'Session Updated' : 'Session Removed') + ': ' + session.sessionId);
|
||||
if (!isAlive) {
|
||||
session = null;
|
||||
}
|
||||
}
|
||||
|
||||
function receiverListener(e) {
|
||||
// Due to API changes just ignore this.
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
if (session != null) {
|
||||
session.sendMessage(namespace, message, onSuccess.bind(this, message), onError);
|
||||
} else {
|
||||
chrome.cast.requestSession(function (e) {
|
||||
session = e;
|
||||
sessionListener(e);
|
||||
session.sendMessage(namespace, message, onSuccess.bind(this, message), onError);
|
||||
}, onError);
|
||||
}
|
||||
}
|
||||
|
||||
// function stopApp() {
|
||||
// session.stop(onStopAppSuccess, onError);
|
||||
// }
|
||||
|
||||
jQuery(function ($) {
|
||||
$(document).on('click', '.cast-url', function () {
|
||||
sendMessage({
|
||||
type: 'load',
|
||||
url: $('#' + $(this).attr('data-target-id')).val()
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -31,20 +31,13 @@ jQuery(function ($) {
|
||||
},
|
||||
always: function (e, data) {
|
||||
const response = data._response.jqXHR;
|
||||
let statusCode = response.status;
|
||||
$button.removeClass('uploading').removeClass('btn-naked btn-super-upload-busy').addClass('btn-info btn-super-upload');
|
||||
|
||||
let errorComment = response.responseText.match(/<!--\s*error=(\d+);\s*-->/);
|
||||
if (errorComment && errorComment[1]) {
|
||||
statusCode = parseInt(errorComment[1], 10);
|
||||
}
|
||||
|
||||
if (statusCode !== 200) {
|
||||
const $alert = $('.alert-upload').removeClass('hidden');
|
||||
if (statusCode === 413) {
|
||||
$alert.html(`<i class="fa fa-warning"></i>${l.js_common_http_error_413}`);
|
||||
if (response.status != 200) {
|
||||
const $alert = $('.alert-danger').removeClass('hidden');
|
||||
if (response.status == 413) {
|
||||
$alert.text(l.js_common_http_error_413);
|
||||
} else {
|
||||
$alert.html(`<i class="fa fa-warning"></i>${l.js_common_http_error_occured.replace('%code%', statusCode)}`);
|
||||
$alert.text(l.js_common_http_error_occured.replace('%code%', response.status));
|
||||
}
|
||||
} else {
|
||||
document.location.reload();
|
||||
@ -10,24 +10,13 @@ jQuery(document).ready(function ($) {
|
||||
});
|
||||
|
||||
$(document).on('click', '.node-player-group-preview', function () {
|
||||
const $icon = $(this).find('i');
|
||||
const isPlay = $icon.hasClass('fa-play');
|
||||
const $holder = $(this).parents('.preview:eq(0)');
|
||||
const $iframe = $('<iframe>', {
|
||||
src: $(this).attr('data-url'),
|
||||
frameborder: 0
|
||||
});
|
||||
|
||||
if (isPlay) {
|
||||
const $iframe = $('<iframe>', {
|
||||
src: $(this).attr('data-url'),
|
||||
frameborder: 0
|
||||
});
|
||||
|
||||
$holder.append($iframe);
|
||||
$(this).addClass('hover-only');
|
||||
$icon.removeClass('fa-play').addClass('fa-pause');
|
||||
} else {
|
||||
$holder.find('iframe').remove();
|
||||
$(this).removeClass('hover-only');
|
||||
$icon.removeClass('fa-pause').addClass('fa-play');
|
||||
}
|
||||
$(this).parents('.preview:eq(0)').append($iframe);
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
$(document).on('click', '.node-player-group-player-assign', function () {
|
||||
|
||||
@ -30,10 +30,6 @@ const hideDropdowns = function () {
|
||||
$('.dropdown').removeClass('dropdown-show');
|
||||
};
|
||||
|
||||
const classColorXor = function(color, fallback) {
|
||||
return color === 'gscaleF' ? 'gscale0' : (color === 'gscale0' ? 'gscaleF' : fallback);
|
||||
};
|
||||
|
||||
const showToast = function (text) {
|
||||
const $toast = $(".toast");
|
||||
$toast.addClass('show');
|
||||
@ -166,12 +162,5 @@ jQuery(document).ready(function ($) {
|
||||
|
||||
showToast(l.js_common_copied);
|
||||
});
|
||||
|
||||
$(window).on('beforeunload', function(event) {
|
||||
$('.modal').each(function() {
|
||||
$(this).find('button[type=submit]').removeClass('hidden');
|
||||
$(this).find('.btn-loading').addClass('hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
(function(){/*
|
||||
|
||||
Copyright The Closure Library Authors.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
'use strict';var l=function(){var a=h,b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}},m=this||self,n=/^[\w+/_-]+[=]{0,2}$/,p=null,q=function(a){return(a=a.querySelector&&a.querySelector("script[nonce]"))&&(a=a.nonce||a.getAttribute("nonce"))&&n.test(a)?a:""},r=function(a,b){function e(){}e.prototype=b.prototype;a.i=b.prototype;a.prototype=new e;a.prototype.constructor=a;a.h=function(c,g,k){for(var f=Array(arguments.length-2),d=2;d<arguments.length;d++)f[d-2]=arguments[d];
|
||||
return b.prototype[g].apply(c,f)}},t=function(a){return a};function u(a){if(Error.captureStackTrace)Error.captureStackTrace(this,u);else{var b=Error().stack;b&&(this.stack=b)}a&&(this.message=String(a))}r(u,Error);u.prototype.name="CustomError";var v=function(a,b){a=a.split("%s");for(var e="",c=a.length-1,g=0;g<c;g++)e+=a[g]+(g<b.length?b[g]:"%s");u.call(this,e+a[c])};r(v,u);v.prototype.name="AssertionError";var w=function(a,b){throw new v("Failure"+(a?": "+a:""),Array.prototype.slice.call(arguments,1));};var x;var A=function(a,b){this.g=b===z?a:""};A.prototype.toString=function(){return this.g+""};var z={};var B=function(){var a=window.navigator.userAgent.match(/Chrome\/([0-9]+)/);return a?parseInt(a[1],10):0},C=function(a){return!!document.currentScript&&(-1!=document.currentScript.src.indexOf("?"+a)||-1!=document.currentScript.src.indexOf("&"+a))},D=function(){return"function"==typeof window.__onGCastApiAvailable?window.__onGCastApiAvailable:null},F=function(a){a.length?E(a.shift(),function(){F(a)}):G()},H=function(a){return"chrome-extension://"+a+"/cast_sender.js"},E=function(a,b,e){var c=document.createElement("script");
|
||||
c.onerror=b;e&&(c.onload=e);if(void 0===x)if(b=null,(e=m.trustedTypes)&&e.createPolicy){try{b=e.createPolicy("goog#html",{createHTML:t,createScript:t,createScriptURL:t})}catch(y){m.console&&m.console.error(y.message)}x=b}else x=b;a=(b=x)?b.createScriptURL(a):a;a=new A(a,z);a:{try{var g=c&&c.ownerDocument,k=g&&(g.defaultView||g.parentWindow);k=k||m;if(k.Element&&k.Location){var f=k;break a}}catch(y){}f=null}if(f&&"undefined"!=typeof f.HTMLScriptElement&&(!c||!(c instanceof f.HTMLScriptElement)&&(c instanceof
|
||||
f.Location||c instanceof f.Element))){f=typeof c;if("object"==f&&null!=c||"function"==f)try{var d=c.constructor.displayName||c.constructor.name||Object.prototype.toString.call(c)}catch(y){d="<object could not be stringified>"}else d=void 0===c?"undefined":null===c?"null":typeof c;w("Argument is not a %s (or a non-Element, non-Location mock); got: %s","HTMLScriptElement",d)}a instanceof A&&a.constructor===A?d=a.g:(d=typeof a,w("expected object of type TrustedResourceUrl, got '"+a+"' of type "+("object"!=
|
||||
d?d:a?Array.isArray(a)?"array":d:"null")),d="type_error:TrustedResourceUrl");c.src=d;(d=c.ownerDocument&&c.ownerDocument.defaultView)&&d!=m?d=q(d.document):(null===p&&(p=q(m.document)),d=p);d&&c.setAttribute("nonce",d);(document.head||document.documentElement).appendChild(c)},I=function(){var a=B(),b=[];if(1<a){var e=a-1;b.push("https://www.gstatic.com/eureka/clank/"+a+"/cast_sender.js");b.push("https://www.gstatic.com/eureka/clank/"+e+"/cast_sender.js")}return b},G=function(){var a=D();a&&a(!1,"No cast extension found")},
|
||||
K=function(){if(J){var a=2,b=D(),e=function(){a--;0==a&&b&&b(!0)};window.__onGCastApiAvailable=e;E("https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js",G,e)}},J=C("loadCastFramework")||C("loadCastApplicationFramework"),L=["pkedcjkdefgpdelpbcmbmeomcjbeemfm","enhhojjnijigcajfphajepfemndkmdlo"];if(0<=window.navigator.userAgent.indexOf("Android")&&0<=window.navigator.userAgent.indexOf("Chrome/")&&window.navigator.presentation){if(60<=B()){K();var M=I();M.push("https://www.gstatic.com/eureka/clank/cast_sender.js");F(M)}}else if(!window.chrome||!window.navigator.presentation||0<=window.navigator.userAgent.indexOf("Edge"))G();else if(89<=B()){K();var N=I(),O=N.push,P=O.apply,h=L.map(H),Q;if(h instanceof Array)Q=h;else{var R,S="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];R=S?S.call(h):
|
||||
{next:l()};for(var T,U=[];!(T=R.next()).done;)U.push(T.value);Q=U}P.call(O,N,Q);N.push("https://www.gstatic.com/eureka/clank/cast_sender.js");F(N)}else K(),F(L.map(H));}).call(this);
|
||||
|
||||
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
@ -21,81 +21,14 @@ jQuery(document).ready(function ($) {
|
||||
});
|
||||
|
||||
$(document).on('click', '.playlist-preview', function () {
|
||||
const $icon = $(this).find('i');
|
||||
const isPlay = $icon.hasClass('fa-play');
|
||||
const $holder = $(this).parents('.preview:eq(0)');
|
||||
const $iframe = $('<iframe>', {
|
||||
src: $(this).attr('data-url'),
|
||||
frameborder: 0
|
||||
});
|
||||
|
||||
if (isPlay) {
|
||||
const $iframe = $('<iframe>', {
|
||||
src: $(this).attr('data-url'),
|
||||
frameborder: 0
|
||||
});
|
||||
|
||||
$holder.append($iframe);
|
||||
$(this).addClass('hover-only');
|
||||
$icon.removeClass('fa-play').addClass('fa-pause');
|
||||
} else {
|
||||
$holder.find('iframe').remove();
|
||||
$(this).removeClass('hover-only');
|
||||
$icon.removeClass('fa-pause').addClass('fa-play');
|
||||
}
|
||||
$(this).parents('.preview:eq(0)').append($iframe);
|
||||
$(this).remove();
|
||||
});
|
||||
//
|
||||
// $(document).on('click', '.cast-scan', function () {
|
||||
// showModal('modal-playlist-cast-scan');
|
||||
// const $modal = $('.modal-playlist-cast-scan:visible');
|
||||
// const $holder = $modal.find('.cast-devices');
|
||||
// const $loading = $modal.find('.loading');
|
||||
//
|
||||
// $loading.removeClass('hidden');
|
||||
// $holder.removeClass('hidden');
|
||||
// $holder.html('');
|
||||
// $loading.html($loading.attr('data-loading'));
|
||||
//
|
||||
// $.ajax({
|
||||
// method: 'GET',
|
||||
// url: route_cast_scan,
|
||||
// headers: {'Content-Type': 'application/json'},
|
||||
// success: function (response) {
|
||||
// $loading.addClass('hidden')
|
||||
//
|
||||
// for (let i = 0; i < response.devices.length; i++) {
|
||||
// const device = response.devices[i];
|
||||
// $holder.append($('<li><a href="javascript:void(0)" class="cast-device" data-id="' + device.friendly_name + '"><i class="fa fa-brands fa-chromecast"></i>' + device.friendly_name + '</a></li>'));
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
// $(document).on('click', '.cast-device', function () {
|
||||
// const $modal = $('.modal-playlist-cast-scan:visible');
|
||||
// const $holder = $modal.find('.cast-devices');
|
||||
// const $loading = $modal.find('.loading');
|
||||
//
|
||||
// $holder.addClass('hidden');
|
||||
// $loading.removeClass('hidden');
|
||||
// $loading.html($loading.attr('data-casting'));
|
||||
//
|
||||
// const id = $(this).attr('data-id');
|
||||
//
|
||||
// $.ajax({
|
||||
// url: route_cast_url,
|
||||
// method: 'POST',
|
||||
// data: JSON.stringify({
|
||||
// device: id,
|
||||
// url: $('#playlist-preview-url').val()
|
||||
// }),
|
||||
// headers: {'Content-Type': 'application/json'},
|
||||
// success: function (response) {
|
||||
// $loading.addClass('hidden');
|
||||
// hideModal();
|
||||
// },
|
||||
// error: function () {
|
||||
// $loading.addClass('hidden');
|
||||
// $holder.removeClass('hidden');
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
main();
|
||||
});
|
||||
|
||||
@ -22,7 +22,7 @@ jQuery(document).ready(function ($) {
|
||||
$('.modal-variable-edit input:visible:eq(0)').focus().select();
|
||||
$('#variable-edit-name').val(variable.name);
|
||||
$('#variable-edit-description').html(variable.description);
|
||||
$('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition === '' || variable.description_edition === null);
|
||||
$('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition === '');
|
||||
$('#variable-edit-value').val(variable.value);
|
||||
$('#variable-edit-id').val(variable.id);
|
||||
});
|
||||
|
||||
@ -1,471 +0,0 @@
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
const DEFAULT_RATIO = "16/9";
|
||||
const contentData = JSON.parse($('#content-edit-location').val() || `{"ratio":"${DEFAULT_RATIO}", "layers":{}}`);
|
||||
let currentElement = null;
|
||||
let elementCounter = 0;
|
||||
let screenRatio = 16/9;
|
||||
|
||||
const setRatio = function () {
|
||||
const ratioString = $('#elem-screen-ratio').val() || DEFAULT_RATIO;
|
||||
$('.ratio-value').text(ratioString.replace('/', ' / '));
|
||||
screenRatio = evalStringRatio(ratioString);
|
||||
$('.screen-holder').css({ 'padding-top': ( 1/ ( screenRatio ) * 100) + '%' });
|
||||
$('.ratio-value').val(screenRatio);
|
||||
$('#screen').css({
|
||||
width: $('#screen').width(),
|
||||
height: $('#screen').width() * (1/screenRatio),
|
||||
position: 'relative',
|
||||
}).parents('.screen-holder:eq(0)').css({
|
||||
width: 'auto',
|
||||
'padding-top': '0px'
|
||||
});
|
||||
};
|
||||
setRatio();
|
||||
|
||||
$(document).on('input', '#elem-screen-ratio', function() {
|
||||
setRatio();
|
||||
});
|
||||
|
||||
function createElement(config = null) {
|
||||
const screen = $('#screen');
|
||||
const screenWidth = screen.width();
|
||||
const screenHeight = screen.height();
|
||||
|
||||
const elementWidth = config ? (config.widthPercent / 100) * screenWidth : 100;
|
||||
const elementHeight = config ? (config.heightPercent / 100) * screenHeight : 50;
|
||||
let x = config ? (config.xPercent / 100) * screenWidth : Math.round(Math.random() * (screenWidth - elementWidth));
|
||||
let y = config ? (config.yPercent / 100) * screenHeight : Math.round(Math.random() * (screenHeight - elementHeight));
|
||||
const zIndex = config ? config.zIndex : elementCounter++;
|
||||
|
||||
//x = Math.round(Math.max(0, Math.min(x, screenWidth - elementWidth)));
|
||||
//y = Math.round(Math.max(0, Math.min(y, screenHeight - elementHeight)));
|
||||
|
||||
const elementId = zIndex;
|
||||
const element = $('<div class="element" id="element-' + zIndex + '" data-id="' + zIndex + '"><i class="fa fa-cog"></i></div>');
|
||||
// const element = $('<div class="element" id="' + elementId + '"><button>Button</button><div class="rotate-handle"></div></div>');
|
||||
|
||||
element.css({
|
||||
left: x,
|
||||
top: y,
|
||||
width: elementWidth,
|
||||
height: elementHeight,
|
||||
zIndex: zIndex,
|
||||
transform: `rotate(0deg)`
|
||||
});
|
||||
|
||||
element.draggable({
|
||||
// containment: "#screen",
|
||||
start: function (event, ui) {
|
||||
focusElement(ui.helper);
|
||||
},
|
||||
drag: function (event, ui) {
|
||||
updateForm(ui.helper);
|
||||
}
|
||||
});
|
||||
|
||||
element.resizable({
|
||||
// containment: "#screen",
|
||||
handles: 'n, s, e, w, nw, ne, sw, se',
|
||||
start: function (event, ui) {
|
||||
focusElement(ui.element);
|
||||
},
|
||||
resize: function (event, ui) {
|
||||
updateForm(ui.element);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
element.rotatable({
|
||||
handle: element.find('.rotate-handle'),
|
||||
rotate: function(event, ui) {
|
||||
updateForm(ui.element);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
element.click(function () {
|
||||
focusElement($(this));
|
||||
});
|
||||
|
||||
screen.append(element);
|
||||
addElementToList(elementId);
|
||||
|
||||
if (config !== null && config.contentId !== null) {
|
||||
element.attr('data-content-id', config.contentId);
|
||||
element.attr('data-content-name', config.contentName);
|
||||
element.attr('data-content-type', config.contentType);
|
||||
element.attr('data-content-metadata', config.contentMetadata);
|
||||
|
||||
applyContentToElement({
|
||||
id: config.contentId,
|
||||
name: config.contentName,
|
||||
type: config.contentType,
|
||||
metadata: config.contentMetadata,
|
||||
}, element);
|
||||
|
||||
updateForm(element);
|
||||
unfocusElements();
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
focusElement(element);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
$(document).on('click', '.element-adjust-aspect-ratio', function(){
|
||||
const metadata = currentElement.data('content-metadata');
|
||||
const ratio = metadata.height / metadata.width;
|
||||
$('#elem-height').val($('#elem-width').val() * ratio).trigger('input');
|
||||
$('#elem-width').val($('#elem-width').val()).trigger('input');
|
||||
});
|
||||
|
||||
$(document).on('click', '.element-list-item', function(){
|
||||
focusElement($('#element-' + $(this).attr('data-id')));
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove-element', function(){
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
removeElementById($(this).attr('data-id'));
|
||||
}
|
||||
});
|
||||
|
||||
function removeElementById(elementId) {
|
||||
$('.element[data-id='+elementId+'], .element-list-item[data-id='+elementId+']').remove();
|
||||
updateZIndexes();
|
||||
}
|
||||
|
||||
function addElementToList(elementId) {
|
||||
const listItem = `<div class="element-list-item" data-id="__ID__">
|
||||
<i class="fa fa-cog"></i>
|
||||
<div class="inner">
|
||||
<label>__EMPTY__ __ID__ </label>
|
||||
<button type="button" class="btn btn-naked remove-element" data-id="__ID__">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-neutral configure-element content-explr-picker" data-id="__ID__">
|
||||
<i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
$('#elementList').append(
|
||||
$(listItem
|
||||
.replace(/__ID__/g, elementId)
|
||||
.replace(/__EMPTY__/g, l.js_common_empty)
|
||||
)
|
||||
);
|
||||
updateZIndexes();
|
||||
}
|
||||
|
||||
function unfocusElements() {
|
||||
$('.element, .element-list-item').removeClass('focused');
|
||||
currentElement = null;
|
||||
updateForm(null);
|
||||
}
|
||||
|
||||
function focusElement($element) {
|
||||
unfocusElements();
|
||||
currentElement = $element;
|
||||
$element.addClass('focused');
|
||||
const listElement = $('.element-list-item[data-id="' + $element.attr('data-id') + '"]');
|
||||
listElement.addClass('focused');
|
||||
updateForm($element);
|
||||
|
||||
const contentType = $element.attr('data-content-type');
|
||||
$('.element-tool').addClass('hidden');
|
||||
|
||||
if (contentType) {
|
||||
if (contentType === 'picture' || contentType === 'video') {
|
||||
const contentMetadata = $element.data('content-metadata');
|
||||
if (contentMetadata.width && contentMetadata.height) {
|
||||
$('.element-tool.element-adjust-aspect-ratio-container').removeClass('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateForm($element) {
|
||||
if (!$element) {
|
||||
$('form#elementForm input').val('').prop('disabled', true);
|
||||
$('.form-element-properties').addClass('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$('.form-element-properties').removeClass('hidden');
|
||||
$('form#elementForm input').prop('disabled', false);
|
||||
|
||||
const offset = $element.position();
|
||||
|
||||
if (offset !== undefined) {
|
||||
$('#elem-x').val(offset.left);
|
||||
$('#elem-y').val(offset.top);
|
||||
$('#elem-width').val($element.width());
|
||||
$('#elem-height').val($element.height());
|
||||
}
|
||||
|
||||
$element.find('i').css('font-size', Math.min($element.width(), $element.height()) / 3);
|
||||
|
||||
/*
|
||||
const rotation = $element.css('transform');
|
||||
const values = rotation.split('(')[1].split(')')[0].split(',');
|
||||
const angle = Math.round(Math.atan2(values[1], values[0]) * (180/Math.PI));
|
||||
$('#elem-rotate').val(angle);
|
||||
*/
|
||||
}
|
||||
|
||||
$(document).on('input', '#elementForm input', function () {
|
||||
if (!currentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let x = Math.round(parseInt($('#elem-x').val()));
|
||||
let y = Math.round(parseInt($('#elem-y').val()));
|
||||
let width = Math.round(parseInt($('#elem-width').val()));
|
||||
let height = Math.round(parseInt($('#elem-height').val()));
|
||||
// let rotation = parseInt($('#elem-rotate').val());
|
||||
|
||||
// Constrain x and y
|
||||
// x = Math.max(0, Math.min(x, screenWidth - width));
|
||||
// y = Math.max(0, Math.min(y, screenHeight - height));
|
||||
|
||||
// Constrain width and height
|
||||
width = Math.min(width, screenWidth - x);
|
||||
height = Math.min(height, screenHeight - y);
|
||||
|
||||
currentElement.css({
|
||||
left: x,
|
||||
top: y,
|
||||
width: width,
|
||||
height: height
|
||||
// transform: `rotate(${rotation}deg)`
|
||||
});
|
||||
|
||||
// Update form values to reflect clamped values
|
||||
$('#elem-x').val(x);
|
||||
$('#elem-y').val(y);
|
||||
$('#elem-width').val(width);
|
||||
$('#elem-height').val(height);
|
||||
});
|
||||
|
||||
// $(document).on('click', '#addElement', function () {
|
||||
// createElement();
|
||||
// });
|
||||
|
||||
$(document).on('click', '#removeAllElements', function () {
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
$('.element, .element-list-item').remove();
|
||||
updateZIndexes();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('dblclick', '.element', function (e) {
|
||||
$('.content-explr-picker[data-id='+$(this).attr('data-id')+']').click();
|
||||
});
|
||||
|
||||
$(document).on('mousedown', function (e) {
|
||||
const keepFocusedElement = $(e.target).hasClass('element')
|
||||
|| $(e.target).hasClass('element-list-item')
|
||||
|| $(e.target).parents('.element:eq(0)').length !== 0
|
||||
|| $(e.target).parents('.element-list-item:eq(0)').length !== 0
|
||||
|| $(e.target).is('input,select,textarea')
|
||||
|| $(e.target).is('.page-panel.right-panel button,a,.btn')
|
||||
|
||||
if (!keepFocusedElement) {
|
||||
unfocusElements();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '#presetGrid2x2', function () {
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let elements = $('.element');
|
||||
if (elements.length < 4) {
|
||||
while (elements.length < 4) {
|
||||
createElement();
|
||||
elements = $('.element');
|
||||
}
|
||||
}
|
||||
|
||||
elements = $('.element-list-item').map(function() {
|
||||
return $('.element[data-id='+$(this).attr('data-id')+']');
|
||||
}).slice(0, 4);
|
||||
|
||||
const gridPositions = [
|
||||
{x: 0, y: 0},
|
||||
{x: screenWidth / 2, y: 0},
|
||||
{x: 0, y: screenHeight / 2},
|
||||
{x: screenWidth / 2, y: screenHeight / 2}
|
||||
];
|
||||
|
||||
elements.each(function (index) {
|
||||
const position = gridPositions[index];
|
||||
$(this).css({
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
width: screenWidth / 2,
|
||||
height: screenHeight / 2
|
||||
});
|
||||
updateForm($(this));
|
||||
});
|
||||
|
||||
unfocusElements();
|
||||
});
|
||||
|
||||
$(document).on('click', '#presetTvNews1x1', function () {
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let elements = $('.element');
|
||||
if (elements.length === 0) {
|
||||
createElement();
|
||||
}
|
||||
|
||||
if (!currentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const height = (screenHeight / 7);
|
||||
currentElement.css({
|
||||
left: 0,
|
||||
top: screenHeight - height,
|
||||
width: screenWidth,
|
||||
height: height
|
||||
});
|
||||
updateForm(currentElement);
|
||||
unfocusElements();
|
||||
});
|
||||
|
||||
$(document).keydown(function (e) {
|
||||
if (e.key === "Escape") {
|
||||
unfocusElements();
|
||||
}
|
||||
|
||||
const hasFocusInInput = $('input,textarea').is(':focus');
|
||||
|
||||
if (!currentElement || hasFocusInInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowLeft") {
|
||||
$('#elem-x').val(parseInt($('#elem-x').val()) - (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowRight") {
|
||||
$('#elem-x').val(parseInt($('#elem-x').val()) + (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowUp") {
|
||||
$('#elem-y').val(parseInt($('#elem-y').val()) - (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowDown") {
|
||||
$('#elem-y').val(parseInt($('#elem-y').val()) + (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "Backspace") {
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
removeElementById(currentElement.attr('data-id'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.content-explr-picker', function () {
|
||||
const elementId = $(this).attr('data-id');
|
||||
const isNew = !elementId;
|
||||
const $element = isNew ? $(createElement()) : $('#element-'+elementId);
|
||||
|
||||
showPickers('modal-content-explr-picker', function (content) {
|
||||
applyContentToElement(content, $element)
|
||||
});
|
||||
});
|
||||
|
||||
const applyContentToElement = function (content, $element) {
|
||||
$element.attr('data-content-id', content.id);
|
||||
$element.attr('data-content-name', content.name);
|
||||
$element.attr('data-content-type', content.type);
|
||||
$element.data('content-metadata', content.metadata);
|
||||
const $elementList = $('.element-list-item[data-id='+$element.attr('data-id')+']');
|
||||
const iconClasses = [
|
||||
'fa',
|
||||
content_type_icon_classes[content.type],
|
||||
content_type_color_classes[content.type]
|
||||
].join(' ');
|
||||
$element.find('i').get(0).classList = iconClasses;
|
||||
$elementList.find('label').text(content.name);
|
||||
$elementList.find('i:eq(0)').get(0).classList = iconClasses;
|
||||
};
|
||||
|
||||
$(document).on('submit', 'form.form', function (e) {
|
||||
unfocusElements();
|
||||
const location = getLocationPayload();
|
||||
$('#content-edit-location').val(JSON.stringify(location));
|
||||
});
|
||||
|
||||
function updateZIndexes() {
|
||||
const zindex = $('.element-list-item').length + 1;
|
||||
$('.element-list-item').each(function(index) {
|
||||
const id = $(this).attr('data-id');
|
||||
$('#element-' + id).css('z-index', zindex - index);
|
||||
});
|
||||
}
|
||||
|
||||
$('#elementList').sortable({
|
||||
update: function(event, ui) {
|
||||
updateZIndexes();
|
||||
}
|
||||
});
|
||||
|
||||
const applyElementsFromContent = function() {
|
||||
for (let i = 0; i < contentData.layers.length; i++) {
|
||||
createElement(contentData.layers[i]);
|
||||
}
|
||||
};
|
||||
|
||||
applyElementsFromContent();
|
||||
|
||||
const getLocationPayload = function() {
|
||||
const screen = $('#screen');
|
||||
const screenWidth = screen.width();
|
||||
const screenHeight = screen.height();
|
||||
const layers = [];
|
||||
|
||||
$('.element').each(function () {
|
||||
const $element = $(this);
|
||||
const offset = $element.position();
|
||||
const x = offset.left;
|
||||
const y = offset.top;
|
||||
const width = $element.width();
|
||||
const height = $element.height();
|
||||
|
||||
const xPercent = (x / screenWidth) * 100;
|
||||
const yPercent = (y / screenHeight) * 100;
|
||||
const widthPercent = (width / screenWidth) * 100;
|
||||
const heightPercent = (height / screenHeight) * 100;
|
||||
const contentId = $element.attr('data-content-id');
|
||||
const contentName = $element.attr('data-content-name');
|
||||
const contentType = $element.attr('data-content-type');
|
||||
const contentMetadata = $element.data('content-metadata');
|
||||
|
||||
const layer = {
|
||||
xPercent: xPercent,
|
||||
yPercent: yPercent,
|
||||
widthPercent: widthPercent,
|
||||
heightPercent: heightPercent,
|
||||
zIndex: parseInt($element.css('zIndex')),
|
||||
contentId: contentId ? parseInt(contentId) : null,
|
||||
contentName: contentName ? contentName : null,
|
||||
contentType: contentType ? contentType : null,
|
||||
contentMetadata: contentMetadata && contentMetadata !== "null" ? contentMetadata : null,
|
||||
};
|
||||
|
||||
layers.push(layer);
|
||||
});
|
||||
|
||||
layers.sort(function(a, b) {
|
||||
return parseInt(b.zIndex) - parseInt(a.zIndex);
|
||||
});
|
||||
|
||||
return {
|
||||
ratio: $('#elem-screen-ratio').val(),
|
||||
layers: layers
|
||||
};
|
||||
};
|
||||
});
|
||||
@ -1,79 +0,0 @@
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
const contentData = JSON.parse($('#content-edit-location').val() || '{}');
|
||||
const screenRatio = 16/9;
|
||||
|
||||
$('.screen-holder').css({
|
||||
'padding-top': ( 1/ ( screenRatio ) * 100) + '%'
|
||||
});
|
||||
|
||||
$('.ratio-value').val(screenRatio);
|
||||
|
||||
$('#screen').css({
|
||||
width: $('#screen').width(),
|
||||
height: $('#screen').height(),
|
||||
position: 'relative',
|
||||
}).parents('.screen-holder:eq(0)').css({
|
||||
width: 'auto',
|
||||
'padding-top': '0px'
|
||||
});
|
||||
|
||||
const draw = function() {
|
||||
const $screen = $('#screen');
|
||||
const $text = $('<div class="text">');
|
||||
let insideText = $('#elem-text').val();
|
||||
|
||||
if ($('#elem-scroll-enable').is(':checked')) {
|
||||
const $wrapper = $('<marquee>');
|
||||
$wrapper.attr({
|
||||
scrollamount: $('#elem-scroll-speed').val(),
|
||||
direction: $('[name=scrollDirection]:checked').val(),
|
||||
behavior: 'scroll',
|
||||
loop: -1
|
||||
});
|
||||
$wrapper.append(insideText);
|
||||
insideText = $wrapper;
|
||||
}
|
||||
|
||||
$text.append(insideText);
|
||||
|
||||
let justifyContent = 'center';
|
||||
switch($('[name=textAlign]:checked').val()) {
|
||||
case 'left': justifyContent = 'flex-start'; break;
|
||||
case 'right': justifyContent = 'flex-end'; break;
|
||||
}
|
||||
|
||||
$text.css({
|
||||
padding: $('#elem-container-margin').val() + 'px',
|
||||
color: $('#elem-fg-color').val(),
|
||||
textAlign: $('[name=textAlign]:checked').val(),
|
||||
textDecoration: $('#elem-text-underline').is(':checked') ? 'underline' : 'normal',
|
||||
fontSize: $('#elem-font-size').val() + 'px',
|
||||
fontWeight: $('#elem-font-bold').is(':checked') ? 'bold' : 'normal',
|
||||
fontStyle: $('#elem-font-italic').is(':checked') ? 'italic' : 'normal',
|
||||
fontFamily: $('#elem-font-family').val() + ", 'Arial', 'sans-serif'",
|
||||
whiteSpace: $('#elem-single-line').is(':checked') ? 'nowrap' : 'normal',
|
||||
justifyContent: justifyContent
|
||||
});
|
||||
|
||||
$screen.css({
|
||||
backgroundColor: $('#elem-bg-color').val(),
|
||||
});
|
||||
|
||||
$screen.html($text);
|
||||
};
|
||||
|
||||
$(document).on('input', '#elementForm input, #elementForm select', function () {
|
||||
draw();
|
||||
});
|
||||
|
||||
draw();
|
||||
|
||||
|
||||
$(document).on('submit', 'form.form', function (e) {
|
||||
const location = $('form#elementForm').serializeObject();
|
||||
$('#content-edit-location').val(JSON.stringify(location));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -21,11 +21,7 @@ jQuery(document).ready(function ($) {
|
||||
$form.find('.object-label:visible').html(optionAttributes['data-object-label'].value);
|
||||
$('.type-icon').attr('class', 'type-icon fa ' + optionAttributes['data-icon'].value);
|
||||
$('.tab-select .widget').attr('class', 'widget ' + ('border-' + color) + ' ' + color);
|
||||
$form.find('button[type=submit]').attr('class', [
|
||||
'btn',
|
||||
`btn-${color}`,
|
||||
classColorXor(color, '')
|
||||
].join(' '));
|
||||
$form.find('button[type=submit]').attr('class', 'btn ' + ('btn-' + color));
|
||||
};
|
||||
|
||||
const main = function () {
|
||||
|
||||
@ -81,9 +81,3 @@ const secondsToHHMMSS = function (seconds) {
|
||||
const secs = seconds % 60;
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const evalStringRatio = function(str) {
|
||||
return str.replace(/(\d+)\/(\d+)/g, function(match, p1, p2) {
|
||||
return (parseInt(p1) / parseInt(p2)).toString();
|
||||
});
|
||||
};
|
||||
|
||||
@ -31,16 +31,8 @@ main {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.context-tail {
|
||||
margin-right: 30px;
|
||||
|
||||
.btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.context-tail-auth {
|
||||
margin-right: 10px;
|
||||
.contex-tail {
|
||||
margin-right: 20px;
|
||||
|
||||
.btn {
|
||||
margin-right: 0;
|
||||
|
||||
@ -26,10 +26,6 @@ body, html {
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
&.fx-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical {
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
@keyframes blink{50%{opacity:0;}}
|
||||
.cfx-blink{animation:1.5s linear infinite blink;}
|
||||
.cfx-ffff-speed {animation-delay: 0.1s;}
|
||||
.cfx-fff-speed {animation-delay: 0.3s;}
|
||||
.cfx-ff-speed {animation-delay: 0.5s;}
|
||||
.cfx-f-speed {animation-delay: 0.8s;}
|
||||
.cfx-m-speed {animation-delay: 1s;}
|
||||
.cfx-s-speed {animation-delay: 1.3s;}
|
||||
.cfx-ss-speed {animation-delay: 1.5s;}
|
||||
.cfx-sss-speed {animation-delay: 1.8s;}
|
||||
.cfx-ssss-speed {animation-delay: 2s;}
|
||||
.cfx-sssss-speed {animation-delay: 3s;}
|
||||
@ -1,10 +1,32 @@
|
||||
.badge-inset {
|
||||
display: inline;
|
||||
color: $gscaleA;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
background: $gscale0;
|
||||
border: 1px solid $gscale3;
|
||||
border-radius: $baseRadius;
|
||||
padding: 3px 7px;
|
||||
a.badge,
|
||||
.badge {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px 5px;
|
||||
border-radius: $baseRadius;
|
||||
font-size: 12px;
|
||||
background: rgba($gscaleF, .1);
|
||||
border: 1px solid transparent;
|
||||
color: $gscaleF;
|
||||
}
|
||||
|
||||
a.badge:hover {
|
||||
color: $gscaleF;
|
||||
border: 1px solid rgba($gscaleF, .4);
|
||||
}
|
||||
|
||||
.panel-inactive .badge {
|
||||
background: rgba($gscale7, .1);
|
||||
color: $gscale7;
|
||||
}
|
||||
|
||||
.panel-inactive a.badge:hover {
|
||||
color: $gscale7;
|
||||
border: 1px solid rgba($gscale7,.2);
|
||||
}
|
||||
|
||||
.badge.anonymous {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
button,
|
||||
.btn {
|
||||
$shadowOffset: 2px;
|
||||
@ -57,7 +56,6 @@ button,
|
||||
box-shadow: 0 $shadowOffset 0 0 darken($gscale5, 10%);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
box-shadow: 0 $shadowOffset 0 1px $gkscale2 inset;
|
||||
background: darken($gscale5, 10%);
|
||||
@ -143,3 +141,4 @@ button,
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,17 +19,3 @@
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinkfade {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -139,30 +139,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.highlighted:hover,
|
||||
&.highlighted {
|
||||
background-color: $seaBlue;
|
||||
|
||||
td {
|
||||
font-weight: bold;
|
||||
color: $gscaleF;
|
||||
|
||||
i.icon-legend {
|
||||
color: $gscaleF;
|
||||
}
|
||||
|
||||
span,
|
||||
i.icon-value {
|
||||
background-color: rgba($gscaleF, .3);
|
||||
color: $gscaleF;
|
||||
}
|
||||
|
||||
&.description {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,24 +81,6 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group,
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0 5px 0 0 !important;
|
||||
justify-content: center !important;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.widget {
|
||||
margin-top: 10px;
|
||||
align-self: stretch;
|
||||
@ -118,11 +100,12 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
input + .btn + .btn {
|
||||
.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&.widget-unit {
|
||||
|
||||
select,
|
||||
input {
|
||||
flex-grow: 0;
|
||||
@ -148,33 +131,6 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
input {
|
||||
&.size-m {
|
||||
max-width: 122px;
|
||||
}
|
||||
|
||||
&.color-picker {
|
||||
max-width: 125px;
|
||||
}
|
||||
|
||||
&.chars-4 {
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
&.chars-3 {
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
&.chars-2 {
|
||||
max-width: 20px;
|
||||
}
|
||||
|
||||
&.chars-1 {
|
||||
max-width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
color: rgba($gscaleF, .7);
|
||||
font-size: 14px;
|
||||
@ -199,17 +155,23 @@ form {
|
||||
color: $gscale5;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid $gscale3;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&.input-naked {
|
||||
padding-left: 0;
|
||||
color: $gscaleB;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&[disabled] {
|
||||
border: none;
|
||||
background: $gscale0;
|
||||
border-radius: $baseRadius;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,20 +18,19 @@
|
||||
@import 'components/modals';
|
||||
@import 'components/toast';
|
||||
@import 'components/dragdrop';
|
||||
@import 'components/animation';
|
||||
|
||||
// Legacy
|
||||
@import 'components/panes';
|
||||
@import 'components/tiles';
|
||||
@import 'components/empty';
|
||||
@import 'components/switches';
|
||||
@import 'components/badges';
|
||||
//@import 'components/badges';
|
||||
|
||||
// Import form styles
|
||||
@import 'forms/forms';
|
||||
|
||||
// Import pages styles
|
||||
@import 'pages/content';
|
||||
@import 'pages/content-composition';
|
||||
@import 'pages/content-text';
|
||||
@import 'pages/logs';
|
||||
@import 'pages/node-player';
|
||||
@import 'pages/playlist';
|
||||
|
||||
@ -43,25 +43,6 @@ button,
|
||||
&.btn-neutral:hover {
|
||||
box-shadow: 0 2px 0 1px $gkscale6 inset;
|
||||
}
|
||||
|
||||
&.btn-neutral {
|
||||
$shadowOffset: 2;
|
||||
color: $gkscale5;
|
||||
background: $white;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
box-shadow: 0 $shadowOffset 0 1px $gkscale2 inset;
|
||||
background: $gkscaleC;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: darken($gscale5, 20%);
|
||||
border: 1px solid $gscaleA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiles .tiles-inner .tile-item {
|
||||
|
||||
@ -1,364 +0,0 @@
|
||||
.view-content-edit.view-content-edit-composition main .main-container {
|
||||
|
||||
.page-panel.left-panel {
|
||||
flex: 1;
|
||||
|
||||
.form-holder {
|
||||
margin: 20px 20px 20px 10px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.page-panel.right-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h3.main {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin-top: 5px;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h4 {
|
||||
margin-right: 5px;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button {
|
||||
padding: 3px 15px;
|
||||
margin:0 3px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
min-height: initial;
|
||||
border: 1px solid $gkscale3;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-holder {
|
||||
//display: flex;
|
||||
//flex-direction: row;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: 56.25%; /* 16:9 aspect ratio */
|
||||
overflow: hidden;
|
||||
border-radius: $baseRadius;
|
||||
outline: 4px solid rgba($gscaleF, .1);
|
||||
|
||||
.screen {
|
||||
background-color: #ddd;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
|
||||
.element {
|
||||
position: absolute !important;
|
||||
background-color: $gkscaleE;
|
||||
outline: 1px solid $gkscaleC;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.focused {
|
||||
border: none;
|
||||
outline: 2px solid $seaBlue;
|
||||
z-index: 89 !important;
|
||||
|
||||
.ui-resizable-handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: $gkscaleC;
|
||||
|
||||
&.fa-cog {
|
||||
text-shadow: 0 -2px $gkscaleB, 0 0px 2px $gkscaleB;
|
||||
}
|
||||
|
||||
&.gscaleF {
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: red;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -15px;
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.ui-resizable-handle {
|
||||
$size: 10px;
|
||||
$sizeOffset: -1*calc($size/2);
|
||||
background: $gkscaleA;
|
||||
border: 1px solid $gkscale5;
|
||||
width: $size;
|
||||
height: $size;
|
||||
z-index: 90;
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
&.ui-resizable-n {
|
||||
cursor: n-resize;
|
||||
top: $sizeOffset;
|
||||
left: 50%;
|
||||
margin-left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-s {
|
||||
cursor: s-resize;
|
||||
bottom: $sizeOffset;
|
||||
left: 50%;
|
||||
margin-left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-w {
|
||||
cursor: w-resize;
|
||||
left: $sizeOffset;
|
||||
top: 50%;
|
||||
margin-top: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-e {
|
||||
cursor: e-resize;
|
||||
right: $sizeOffset;
|
||||
top: 50%;
|
||||
margin-top: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-nw {
|
||||
cursor: nw-resize;
|
||||
top: $sizeOffset;
|
||||
left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-ne {
|
||||
cursor: ne-resize;
|
||||
top: $sizeOffset;
|
||||
right: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-sw {
|
||||
cursor: sw-resize;
|
||||
bottom: $sizeOffset;
|
||||
left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-se {
|
||||
cursor: se-resize;
|
||||
bottom: $sizeOffset;
|
||||
right: $sizeOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.elements-holder {
|
||||
align-self: stretch;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin: 0 0 20px 0;
|
||||
|
||||
&.divide {
|
||||
border-top: 1px solid $gscale2;
|
||||
margin-top: 10px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-elements-list {
|
||||
padding: 10px;
|
||||
background: $gscale2;
|
||||
border-radius: $baseRadius;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-self: flex-start;
|
||||
|
||||
.element-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
> i {
|
||||
color: $gscaleE;
|
||||
margin:0 10px 0 0;
|
||||
cursor: move;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inner:hover,
|
||||
&.focused .inner {
|
||||
background-color: $seaBlue;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
|
||||
button.btn-naked {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
cursor: pointer;
|
||||
padding: 5px 5px 5px 10px;
|
||||
margin-bottom: 5px;
|
||||
background: $gkscaleE;
|
||||
border-radius: $baseRadius;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
color: $gkscale2;
|
||||
min-height: 46px;
|
||||
flex: 1;
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 219px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
display: none;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
button.btn-naked {
|
||||
color: $gscale5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
label {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-element-properties {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.divide {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
|
||||
&[disabled] {
|
||||
padding: 8px 0 5px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, .05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,155 +0,0 @@
|
||||
.view-content-edit.view-content-edit-text main .main-container {
|
||||
|
||||
.page-panel.left-panel {
|
||||
flex: 1;
|
||||
|
||||
.form-holder {
|
||||
margin: 20px 20px 20px 10px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.page-panel.right-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h3.main {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin-top: 5px;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.screen-holder {
|
||||
//display: flex;
|
||||
//flex-direction: row;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: 56.25%; /* 16:9 aspect ratio */
|
||||
overflow: hidden;
|
||||
border-radius: $baseRadius;
|
||||
outline: 4px solid rgba($gscaleF, .1);
|
||||
background: repeating-conic-gradient(#EEE 0% 25%, white 0% 50%) 50% / 20px 20px;
|
||||
|
||||
.screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: flex;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
|
||||
marquee {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-element-properties {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.divide {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
label {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
|
||||
&[disabled] {
|
||||
padding: 8px 0 5px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, .05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -23,29 +23,6 @@
|
||||
|
||||
.view-content-edit main .main-container {
|
||||
|
||||
.top-content {
|
||||
h3 {
|
||||
color: $gscaleF;
|
||||
padding: 10px 10px 10px 0;
|
||||
font-size: 16px;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: $baseRadius;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-content {
|
||||
.page-content {
|
||||
flex: 1;
|
||||
@ -61,11 +38,32 @@
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
color: $gscaleF;
|
||||
padding: 10px 10px 10px 0;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
align-self: stretch;
|
||||
margin-left: -8px;
|
||||
|
||||
span {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: $baseRadius;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -89,3 +87,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -75,27 +75,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.preview-holder {
|
||||
position: relative;
|
||||
|
||||
.preview-holder {
|
||||
.form-group {
|
||||
flex-grow: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hover-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.hover-only {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
&:hover {
|
||||
background: $gkscaleC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
||||
@ -11,75 +11,25 @@
|
||||
align-self: stretch;
|
||||
color: $gscale6;
|
||||
}
|
||||
//
|
||||
//.modal-playlist-cast-scan {
|
||||
// h2 {
|
||||
// text-align: left;
|
||||
// }
|
||||
//
|
||||
// .alert {
|
||||
// padding: 10px;
|
||||
// font-size: 12px;
|
||||
// margin-bottom: 20px;
|
||||
// display: block;
|
||||
// text-align: center;
|
||||
//
|
||||
// i {
|
||||
// margin-right: 5px;
|
||||
// }
|
||||
//
|
||||
// a {
|
||||
// margin: 0;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// .loading {
|
||||
// color: $gscaleF;
|
||||
// animation-duration: 2s;
|
||||
// animation-iteration-count: infinite;
|
||||
// animation-name: blinkfade;
|
||||
// }
|
||||
//
|
||||
// ul.cast-devices {
|
||||
// list-style: none;
|
||||
// margin: 0;
|
||||
// padding: 0;
|
||||
//
|
||||
// li {
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// justify-content: flex-start;
|
||||
// align-items: center;
|
||||
// list-style: none;
|
||||
// border-bottom: 1px solid $gscale2;
|
||||
// border-radius: $baseRadius;
|
||||
//
|
||||
// a {
|
||||
// flex: 1;
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// justify-content: flex-start;
|
||||
// align-items: center;
|
||||
// padding: 20px 15px;
|
||||
// color: $gscaleF;
|
||||
// align-self: stretch;
|
||||
//
|
||||
// i {
|
||||
//
|
||||
// margin-right: 10px;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// &:hover {
|
||||
// background: $gscale2;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// li:last-child {
|
||||
// border: none;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
.modal-playlist-qrcode {
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-pic {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
border: 4px solid $gscale5;
|
||||
border-radius: $baseRadius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-slide {
|
||||
h2 {
|
||||
@ -148,28 +98,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.preview-holder {
|
||||
position: relative;
|
||||
|
||||
.preview-holder {
|
||||
.form-group {
|
||||
flex-grow: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hover-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.hover-only {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
||||
&:hover {
|
||||
background: $gkscaleC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
||||
@ -15,35 +15,9 @@
|
||||
}
|
||||
|
||||
.tile-tail {
|
||||
.btn {
|
||||
a:last-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tile-metrics {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.widget,
|
||||
.form-group {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ $layoutBorder: 1px solid $gscale2;
|
||||
// Packs
|
||||
$colors: (
|
||||
warning: $warning,
|
||||
orange: $orange,
|
||||
info: $info,
|
||||
info-alt: $bitterBlue,
|
||||
success: $success,
|
||||
@ -40,8 +39,6 @@ $colors: (
|
||||
redhat:$redhat,
|
||||
centos:$centos,
|
||||
other:$other,
|
||||
gscale0:$gscale0,
|
||||
gscaleF:$gscaleF,
|
||||
);
|
||||
|
||||
// Classes
|
||||
|
||||
@ -10,8 +10,11 @@ services:
|
||||
- DEBUG=false
|
||||
- SECRET_KEY=ANY_SECRET_KEY_HERE
|
||||
- PORT=5000
|
||||
- PORT_HTTP_EXTERNAL_STORAGE=5001
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- ./:/app/
|
||||
- ./data/db:/app/data/db
|
||||
- ./data/uploads:/app/data/uploads
|
||||
- ./var/run/storage:/app/var/run/storage
|
||||
ports:
|
||||
- 5000:5000
|
||||
- 5001:5001
|
||||
|
||||
@ -2,16 +2,17 @@ services:
|
||||
webapp:
|
||||
container_name: obscreen
|
||||
restart: unless-stopped
|
||||
image: csmith1865/obscreen:latest
|
||||
image: jierka/obscreen:latest
|
||||
environment:
|
||||
- DEMO=false
|
||||
- DEBUG=false
|
||||
- SECRET_KEY=ANY_SECRET_KEY_HERE
|
||||
- PORT=5000
|
||||
- PORT_HTTP_EXTERNAL_STORAGE=5001
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- ./data/db:/app/data/db
|
||||
- ./data/uploads:/app/data/uploads
|
||||
- ./var/run/storage:/app/var/run/storage
|
||||
ports:
|
||||
- 5000:5000
|
||||
- 5001:5001
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
user nginx;
|
||||
worker_processes 4;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log /var/log/nginx/access.log;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 200G;
|
||||
|
||||
autoindex on;
|
||||
|
||||
server {
|
||||
root /var/www/html/public;
|
||||
listen 80 default_server;
|
||||
listen 443 ssl default_server;
|
||||
|
||||
ssl_certificate /ssl/ssl-cert-snakeoil.pem;
|
||||
ssl_certificate_key /ssl/ssl-cert-snakeoil.key;
|
||||
|
||||
location / {
|
||||
proxy_connect_timeout 60;
|
||||
proxy_read_timeout 60;
|
||||
proxy_send_timeout 60;
|
||||
proxy_intercept_errors on;
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,20 +62,10 @@ docker compose up --detach --pull=always
|
||||
#### Install
|
||||
- Install studio by executing following script
|
||||
|
||||
##### Linux
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-studio.sh -o /tmp/install-studio.sh && chmod +x /tmp/install-studio.sh && sudo /bin/bash /tmp/install-studio.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
##### Windows & MacOS
|
||||
```bash
|
||||
git clone https://github.com/jr-k/obscreen.git
|
||||
cd obscreen
|
||||
python3 -m venv venv
|
||||
source ./venv/bin/activate
|
||||
pip install .
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
#### Configure
|
||||
- Server configuration is editable in `.env` file.
|
||||
@ -120,23 +110,7 @@ When you run the browser yourself, don't forget to use these flags for chromium
|
||||
```bash
|
||||
# chromium or chromium-browser or even chrome
|
||||
# replace http://localhost:5000 with your obscreen-studio instance url
|
||||
chromium \
|
||||
--disk-cache-size=2147483648 \
|
||||
--disable-features=Translate \
|
||||
--ignore-certificate-errors \
|
||||
--disable-web-security \
|
||||
--disable-restore-session-state \
|
||||
--autoplay-policy=no-user-gesture-required \
|
||||
--start-maximized \
|
||||
--allow-running-insecure-content \
|
||||
--remember-cert-error-decisions \
|
||||
--noerrdialogs \
|
||||
--kiosk \
|
||||
--incognito \
|
||||
--window-position=0,0 \
|
||||
--window-size=1920,1080 \
|
||||
--display=:0 \
|
||||
http://localhost:5000
|
||||
chromium --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=1920,1080 --display=:0 http://localhost:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# <img src="https://raw.githubusercontent.com/csmith1865/obscreen/refs/heads/master/docs/img/obscreen.png" width="22"> Obscreen - Autorun on RaspberryPi
|
||||
# <img src="https://github.com/jr-k/obscreen/blob/master/docs/img/obscreen.png" width="22"> Obscreen - Autorun on RaspberryPi
|
||||
|
||||
> #### 👈 [back to readme](../README.md)
|
||||
> #### 👈 [back to readme](/README.md)
|
||||
|
||||
#### 🔴 You want to power RaspberryPi and automatically see your slideshow on a screen connected to it and manage your slideshow ? You're in the right place.
|
||||
|
||||
@ -20,20 +20,10 @@
|
||||
#### Install
|
||||
- Install studio by executing following script
|
||||
|
||||
##### Linux
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/csmith1865/obscreen/master/system/install-studio.sh -o /tmp/install-studio.sh && chmod +x /tmp/install-studio.sh && sudo /bin/bash /tmp/install-studio.sh $USER $HOME
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-studio.sh -o /tmp/install-studio.sh && chmod +x /tmp/install-studio.sh && sudo /bin/bash /tmp/install-studio.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
##### Windows & MacOS
|
||||
```bash
|
||||
git clone https://github.com/csmith1865/obscreen.git
|
||||
cd obscreen
|
||||
python3 -m venv venv
|
||||
source ./venv/bin/activate
|
||||
pip install .
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
#### Configure
|
||||
- Server configuration is editable in `.env` file.
|
||||
@ -82,7 +72,7 @@ docker run --restart=always --name obscreen --pull=always \
|
||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||
|
||||
# Download docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/csmith1865/obscreen/master/docker-compose.yml > docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/docker-compose.yml > docker-compose.yml
|
||||
|
||||
# Run
|
||||
docker compose up --detach --pull=always
|
||||
@ -106,7 +96,7 @@ docker compose up --detach --pull=always
|
||||
#### How to install
|
||||
- Install player autorun by executing following script (will install chromium, x11, pulseaudio and obscreen-player systemd service)
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/csmith1865/obscreen/master/system/install-player-rpi.sh -o /tmp/install-player-rpi.sh && chmod +x /tmp/install-player-rpi.sh && sudo /bin/bash /tmp/install-player-rpi.sh $USER $HOME
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-player-rpi.sh -o /tmp/install-player-rpi.sh && chmod +x /tmp/install-player-rpi.sh && sudo /bin/bash /tmp/install-player-rpi.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
@ -114,7 +104,7 @@ sudo reboot
|
||||
1. Just use systemctl `sudo systemctl restart obscreen-player.service`
|
||||
|
||||
#### How to enable sound
|
||||
1. First you have to reboot your device if you never did after obscreen player installation; with command `sudo reboot`
|
||||
1. First you have to reboot your device with `sudo reboot`
|
||||
2. You have to set audio channel to HDMI `sudo raspi-config nonint do_audio 1` (0 is for jack 3.5 output)
|
||||
|
||||
---
|
||||
@ -128,23 +118,7 @@ When you run the browser yourself, don't forget to use these flags for chromium
|
||||
```bash
|
||||
# chromium or chromium-browser or even chrome
|
||||
# replace http://localhost:5000 with your obscreen-studio instance url
|
||||
chromium \
|
||||
--disk-cache-size=2147483648 \
|
||||
--disable-features=Translate \
|
||||
--ignore-certificate-errors \
|
||||
--disable-web-security \
|
||||
--disable-restore-session-state \
|
||||
--autoplay-policy=no-user-gesture-required \
|
||||
--start-maximized \
|
||||
--allow-running-insecure-content \
|
||||
--remember-cert-error-decisions \
|
||||
--noerrdialogs \
|
||||
--kiosk \
|
||||
--incognito \
|
||||
--window-position=0,0 \
|
||||
--window-size=1920,1080 \
|
||||
--display=:0 \
|
||||
http://localhost:5000
|
||||
chromium --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=1920,1080 --display=:0 http://localhost:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
31
lang/en.json
31
lang/en.json
@ -59,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Are you sure?",
|
||||
"slideshow_content_page_title": "Content Library",
|
||||
"slideshow_content_button_add": "New Content",
|
||||
"slideshow_content_referenced_in_slide_error": "Content '%contentName%' is referenced in a slide, remove slide first",
|
||||
"slideshow_content_referenced_in_slide_error": "Content is referenced in a slide, remove slide first",
|
||||
"slideshow_content_panel_active": "Content",
|
||||
"slideshow_content_panel_empty": "Currently, there are no content. %link% now.",
|
||||
"slideshow_content_panel_th_name": "Name",
|
||||
@ -105,7 +105,6 @@
|
||||
"js_playlist_delete_confirmation": "Are you sure?",
|
||||
"playlist_delete_has_slides": "Playlist has slides, please remove them before and retry",
|
||||
"playlist_delete_has_node_player_groups": "Playlist is linked to a playgroup",
|
||||
"playlist_cast_warning": "Your <a href=\"%href%\" target=\"_blank\">external URL</a> must be served over https for this to work",
|
||||
"fleet_node_player_page_title": "Players",
|
||||
"fleet_node_player_button_add": "Add a player",
|
||||
"fleet_node_player_panel_active": "Active players",
|
||||
@ -184,12 +183,12 @@
|
||||
"settings_variable_desc_auth_enabled": "Enable auth management",
|
||||
"settings_variable_desc_edition_auth_enabled": "Default user credentials will be %username%/%password%",
|
||||
"settings_variable_desc_external_url": "External url (i.e: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_external_storage_url": "External url for external storage (i.e: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_slide_upload_limit": "Slide upload limit (in megabytes)",
|
||||
"settings_variable_desc_dark_mode": "Dark mode",
|
||||
"settings_variable_desc_intro_slide_duration": "Introduction slide duration (in seconds)",
|
||||
"settings_variable_desc_default_slide_time_with_seconds": "Show the seconds on the clock in the introduction slide",
|
||||
"settings_variable_desc_polling_interval": "Refresh interval applied for settings to the player (in seconds)",
|
||||
"settings_variable_desc_player_content_cache": "Enable cache",
|
||||
"settings_variable_desc_slide_animation_enabled": "Enable animation effect between slides",
|
||||
"settings_variable_desc_slide_animation_entrance_effect": "Slide animation entrance effect",
|
||||
"settings_variable_desc_slide_animation_exit_effect": "Slide animation exit effect (generally better off without it)",
|
||||
@ -237,7 +236,6 @@
|
||||
"common_pick_element": "Pick an element",
|
||||
"common_untitled": "<untitled>",
|
||||
"common_loading": "Loading...",
|
||||
"common_casting": "Casting...",
|
||||
"common_default_node_player_group": "Default Playgroup",
|
||||
"common_default_playlist": "Default Playlist",
|
||||
"common_unknown_ipaddr": "Unknown IP address",
|
||||
@ -255,27 +253,12 @@
|
||||
"common_apply": "Apply",
|
||||
"common_saved": "Changes have been saved",
|
||||
"common_new_folder": "New Folder",
|
||||
"common_folder_not_empty_error": "Folder '%folderName%' isn't empty, you must delete its content first",
|
||||
"common_folder_not_empty_error": "Folder isn't empty, you must delete its content first",
|
||||
"common_copied": "Element copied in clipboard!",
|
||||
"common_host_placeholder": "raspberrypi.local or 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Error %code% occured",
|
||||
"common_http_error_413": "Files are too large",
|
||||
"common_width": "Width",
|
||||
"common_height": "Height",
|
||||
"common_position": "Position",
|
||||
"common_angle": "Angle",
|
||||
"common_size": "Dimensions",
|
||||
"composition_elements_heading": "Elements",
|
||||
"composition_element_add": "Add element",
|
||||
"composition_elements_delete_all": "Delete all",
|
||||
"composition_presets": "Presets",
|
||||
"composition_presets_grid_2x2": "Grid 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Screen",
|
||||
"composition_element_x_axis": "X axis",
|
||||
"composition_element_y_axis": "Y axis",
|
||||
"composition_element_match_content_aspect_ratio": "Match content aspect ratio",
|
||||
"logout": "Logout",
|
||||
"login_error_not_found": "Bad credentials",
|
||||
"login_error_bad_credentials": "Bad credentials",
|
||||
@ -307,10 +290,6 @@
|
||||
"enum_content_type_external_storage": "External Storage",
|
||||
"enum_content_type_external_storage_object_label": "Specify an existing directory relative to the following path",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Path relative to a removeable device",
|
||||
"enum_content_type_composition": "Composition",
|
||||
"enum_content_type_composition_object_label": "Screen aspect ratio",
|
||||
"enum_content_type_text": "Text",
|
||||
"enum_content_type_text_object_label": "Displayed text",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Picture",
|
||||
@ -329,8 +308,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Other",
|
||||
"sysinfo_device_model": "Device model",
|
||||
"sysinfo_device_model_unknown": "Unknown model",
|
||||
"sysinfo_rpi_model": "Raspberry Pi Model",
|
||||
"sysinfo_rpi_model_unknown": "Not a Raspberry Pi or model information not available",
|
||||
"sysinfo_storage_free_space": "Storage Free Space",
|
||||
"sysinfo_memory_usage": "Memory Usage",
|
||||
"sysinfo_os_version": "OS Version",
|
||||
|
||||
31
lang/es.json
31
lang/es.json
@ -59,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "¿Estás seguro?",
|
||||
"slideshow_content_page_title": "Biblioteca de contenidos",
|
||||
"slideshow_content_button_add": "Nuevo Contenido",
|
||||
"slideshow_content_referenced_in_slide_error": "Se hace referencia al contenido '%contentName%' en una diapositiva; elimine la diapositiva primero",
|
||||
"slideshow_content_referenced_in_slide_error": "Se hace referencia al contenido en una diapositiva; elimine la diapositiva primero",
|
||||
"slideshow_content_panel_active": "Contenido",
|
||||
"slideshow_content_panel_empty": "Actualmente, no hay contenido. %link% ahora.",
|
||||
"slideshow_content_panel_th_name": "Nombre",
|
||||
@ -105,7 +105,6 @@
|
||||
"js_playlist_delete_confirmation": "¿Estás seguro?",
|
||||
"playlist_delete_has_slides": "La playlist tiene diapositivas, por favor elimínelas antes y reintente",
|
||||
"playlist_delete_has_node_player_groups": "La playlist está asignada a un playgroup",
|
||||
"playlist_cast_warning": "Tu <a href=\"%href%\" target=\"_blank\">URL externa</a> debe ser entregada en https para que esto funcione",
|
||||
"fleet_node_player_page_title": "Reproductores",
|
||||
"fleet_node_player_button_add": "Agregar un reproductor",
|
||||
"fleet_node_player_panel_active": "Reproductores activos",
|
||||
@ -185,12 +184,12 @@
|
||||
"settings_variable_desc_auth_enabled": "Habilitar gestión de autenticación",
|
||||
"settings_variable_desc_edition_auth_enabled": "Las credenciales predeterminadas del usuario serán %username%/%password%",
|
||||
"settings_variable_desc_external_url": "URL externa (ej.: https://studio-01.company.com o http://10.10.3.100)",
|
||||
"settings_variable_desc_external_storage_url": "URL externa para almacenamiento externo(ej.: https://studio-01.company.com o http://10.10.3.100)",
|
||||
"settings_variable_desc_slide_upload_limit": "Límite de carga de diapositivas (en megabytes)",
|
||||
"settings_variable_desc_dark_mode": "Modo oscuro",
|
||||
"settings_variable_desc_intro_slide_duration": "Duración de la diapositiva de introducción (en segundos)",
|
||||
"settings_variable_desc_default_slide_time_with_seconds": "Mostrar los segundos en el reloj de la diapositiva de introducción",
|
||||
"settings_variable_desc_polling_interval": "Intervalo de actualización aplicado para configuraciones del reproductor (en segundos)",
|
||||
"settings_variable_desc_player_content_cache": "Habilitar la caché",
|
||||
"settings_variable_desc_slide_animation_enabled": "Habilitar efecto de animación entre diapositivas",
|
||||
"settings_variable_desc_slide_animation_entrance_effect": "Efecto de entrada de animación de diapositiva",
|
||||
"settings_variable_desc_slide_animation_exit_effect": "Efecto de salida de animación de diapositiva (generalmente mejor sin él)",
|
||||
@ -238,7 +237,6 @@
|
||||
"common_pick_element": "Elige un elemento",
|
||||
"common_untitled": "<sin-título>",
|
||||
"common_loading": "Cargando...",
|
||||
"common_casting": "Casting...",
|
||||
"common_default_node_player_group": "Playgroup predeterminado",
|
||||
"common_default_playlist": "Lista de reproducción predeterminada",
|
||||
"common_unknown_ipaddr": "Dirección IP desconocida",
|
||||
@ -256,27 +254,12 @@
|
||||
"common_apply": "Aplicar",
|
||||
"common_saved": "Los cambios se han guardado",
|
||||
"common_new_folder": "Nuevo Carpeta",
|
||||
"common_folder_not_empty_error": "La carpeta '%folderName%' no está vacía, primero debes eliminar su contenido",
|
||||
"common_folder_not_empty_error": "La carpeta no está vacía, primero debes eliminar su contenido",
|
||||
"common_copied": "¡Elemento copiado!",
|
||||
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Se ha producido un error %code%",
|
||||
"common_http_error_413": "Los archivos son demasiado grandes",
|
||||
"common_width": "Ancho",
|
||||
"common_height": "Altura",
|
||||
"common_position": "Posición",
|
||||
"common_angle": "Ángulo",
|
||||
"common_size": "Dimensiones",
|
||||
"composition_elements_heading": "Elementos",
|
||||
"composition_element_add": "Añadir elemento",
|
||||
"composition_elements_delete_all": "Eliminar todo",
|
||||
"composition_presets": "Preajustes",
|
||||
"composition_presets_grid_2x2": "Cuadrícula 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Pantalla",
|
||||
"composition_element_x_axis": "Eje X",
|
||||
"composition_element_y_axis": "Eje Y",
|
||||
"composition_element_match_content_aspect_ratio": "Ajustar la escala del contenido",
|
||||
"logout": "Cerrar sesión",
|
||||
"login_error_not_found": "Credenciales incorrectas",
|
||||
"login_error_bad_credentials": "Credenciales incorrectas",
|
||||
@ -308,10 +291,6 @@
|
||||
"enum_content_type_external_storage": "Almacenamiento externo",
|
||||
"enum_content_type_external_storage_object_label": "Especifique un directorio existente relativo a la siguiente ruta",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Ruta relativa a un dispositivo extraíble",
|
||||
"enum_content_type_composition": "Composición",
|
||||
"enum_content_type_composition_object_label": "Relación de aspecto de la pantalla",
|
||||
"enum_content_type_text": "Texto",
|
||||
"enum_content_type_text_object_label": "Texto mostrado",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Imagen",
|
||||
@ -330,8 +309,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Otro",
|
||||
"sysinfo_device_model": "Modelo del dispositivo",
|
||||
"sysinfo_device_model_unknown": "Modelo desconocido",
|
||||
"sysinfo_rpi_model": "Modelo de Raspberry Pi",
|
||||
"sysinfo_rpi_model_unknown": "No es una Raspberry Pi o la información del modelo no está disponible",
|
||||
"sysinfo_storage_free_space": "Espacio de almacenamiento libre",
|
||||
"sysinfo_memory_usage": "Uso de memoria",
|
||||
"sysinfo_os_version": "Versión del SO",
|
||||
|
||||
31
lang/fr.json
31
lang/fr.json
@ -59,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
|
||||
"slideshow_content_page_title": "Bibliothèque de contenus",
|
||||
"slideshow_content_button_add": "Nouveau Contenu",
|
||||
"slideshow_content_referenced_in_slide_error": "Le contenu '%contentName%' est référencé dans une slide, supprimez d'abord la slide",
|
||||
"slideshow_content_referenced_in_slide_error": "Le contenu est référencé dans une slide, supprimez d'abord la slide",
|
||||
"slideshow_content_panel_active": "Contenus",
|
||||
"slideshow_content_panel_empty": "Actuellement, il n'y a aucun contenu. %link% maintenant.",
|
||||
"slideshow_content_panel_th_name": "Nom",
|
||||
@ -106,7 +106,6 @@
|
||||
"js_playlist_delete_confirmation": "Êtes-vous sûr ?",
|
||||
"playlist_delete_has_slides": "La playlist contient des slides, supprimez-les avant et réessayez",
|
||||
"playlist_delete_has_node_player_groups": "La playlist est attribuée à un playgroup",
|
||||
"playlist_cast_warning": "Votre <a href=\"%href%\" target=\"_blank\">URL externe</a> doit être servi en https pour que ça fonctionne",
|
||||
"fleet_node_player_page_title": "Lecteurs",
|
||||
"fleet_node_player_button_add": "Ajouter un lecteur",
|
||||
"fleet_node_player_panel_active": "Players actifs",
|
||||
@ -186,12 +185,12 @@
|
||||
"settings_variable_desc_auth_enabled": "Activer la gestion de l'authentification",
|
||||
"settings_variable_desc_edition_auth_enabled": "Les identifiants de l'utilisateur par défaut seront %username%/%password%",
|
||||
"settings_variable_desc_external_url": "URL externe (i.e: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_external_storage_url": "URL externe pour le stockage externe (i.e: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_slide_upload_limit": "Limite d'upload du fichier d'une slide (en mégaoctets)",
|
||||
"settings_variable_desc_dark_mode": "Mdoe sombre",
|
||||
"settings_variable_desc_intro_slide_duration": "Durée de la slide d'introduction (en secondes)",
|
||||
"settings_variable_desc_default_slide_time_with_seconds": "Afficher les secondes de l'horloge de la slide d'introduction",
|
||||
"settings_variable_desc_polling_interval": "Intervalle de rafraîchissement des paramètres à appliquer au lecteur (en secondes)",
|
||||
"settings_variable_desc_player_content_cache": "Activer le cache",
|
||||
"settings_variable_desc_slide_animation_enabled": "Activer les effets d'animation entre les slides",
|
||||
"settings_variable_desc_slide_animation_entrance_effect": "Effet d'animation d'arrivée de la slide",
|
||||
"settings_variable_desc_slide_animation_exit_effect": "Effet d'animation de sortie de la slide (généralement mieux sans)",
|
||||
@ -239,7 +238,6 @@
|
||||
"common_pick_element": "Choisissez un élément",
|
||||
"common_untitled": "<sans-titre>",
|
||||
"common_loading": "Chargement...",
|
||||
"common_casting": "Casting...",
|
||||
"common_default_node_player_group": "Playgroup par défaut",
|
||||
"common_default_playlist": "Playlist par défaut",
|
||||
"common_unknown_ipaddr": "Adresse IP inconnue",
|
||||
@ -257,27 +255,12 @@
|
||||
"common_apply": "Appliquer",
|
||||
"common_saved": "Les modifications ont été enregistrées",
|
||||
"common_new_folder": "Nouveau Dossier",
|
||||
"common_folder_not_empty_error": "Le dossier '%folderName%' n'est pas vide, vous devez d'abord supprimer son contenu",
|
||||
"common_folder_not_empty_error": "Le dossier n'est pas vide, vous devez d'abord supprimer son contenu",
|
||||
"common_copied": "Element copié !",
|
||||
"common_host_placeholder": "raspberrypi.local ou 192.168.1.85",
|
||||
"common_reachable_at": "Hôte",
|
||||
"common_http_error_occured": "Une erreur %code% est apparue",
|
||||
"common_http_error_413": "Les fichiers sont trop volumineux",
|
||||
"common_width": "Largeur",
|
||||
"common_height": "Hauteur",
|
||||
"common_position": "Position",
|
||||
"common_angle": "Angle",
|
||||
"common_size": "Dimensions",
|
||||
"composition_elements_heading": "Éléments",
|
||||
"composition_element_add": "Ajouter un élément",
|
||||
"composition_elements_delete_all": "Tout supprimer",
|
||||
"composition_presets": "Préréglages",
|
||||
"composition_presets_grid_2x2": "Grille 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Écran",
|
||||
"composition_element_x_axis": "Axe X",
|
||||
"composition_element_y_axis": "Axe Y",
|
||||
"composition_element_match_content_aspect_ratio": "Ajuster l'échelle du contenu",
|
||||
"logout": "Déconnexion",
|
||||
"login_error_not_found": "Identifiants invalides",
|
||||
"login_error_bad_credentials": "Identifiants invalides",
|
||||
@ -309,10 +292,6 @@
|
||||
"enum_content_type_external_storage": "Stockage externe",
|
||||
"enum_content_type_external_storage_object_label": "Spécifiez un répertoire existant par rapport au chemin suivant",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Chemin relatif à un périphérique amovible",
|
||||
"enum_content_type_composition": "Composition",
|
||||
"enum_content_type_composition_object_label": "Rapport hauteur/largeur de l'écran",
|
||||
"enum_content_type_text": "Texte",
|
||||
"enum_content_type_text_object_label": "Texte affiché",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Vidéo",
|
||||
"enum_content_type_picture": "Image",
|
||||
@ -331,8 +310,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Autre",
|
||||
"sysinfo_device_model": "Modèle de l'appareil",
|
||||
"sysinfo_device_model_unknown": "Modèle inconnu",
|
||||
"sysinfo_rpi_model": "Modèle du Raspberry Pi",
|
||||
"sysinfo_rpi_model_unknown": "Le modèle n'est pas un Raspberry Pi",
|
||||
"sysinfo_storage_free_space": "Stockage Disponible",
|
||||
"sysinfo_memory_usage": "Utilisation Mémoire",
|
||||
"sysinfo_os_version": "Version SE",
|
||||
|
||||
31
lang/it.json
31
lang/it.json
@ -59,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Sei sicuro?",
|
||||
"slideshow_content_page_title": "Libreria dei contenuti",
|
||||
"slideshow_content_button_add": "Nuovo Contenuto",
|
||||
"slideshow_content_referenced_in_slide_error": "Si fa riferimento al contenuto '%contentName%' in una diapositiva, rimuovere prima la diapositiva",
|
||||
"slideshow_content_referenced_in_slide_error": "Si fa riferimento al contenuto in una diapositiva, rimuovere prima la diapositiva",
|
||||
"slideshow_content_panel_active": "Contenuti",
|
||||
"slideshow_content_panel_empty": "Attualmente non ci sono contenuti. %link% adesso.",
|
||||
"slideshow_content_panel_th_name": "Nome",
|
||||
@ -105,7 +105,6 @@
|
||||
"js_playlist_delete_confirmation": "Sei sicuro?",
|
||||
"playlist_delete_has_slides": "Sono presenti slide nella playlist, annullale e riprova",
|
||||
"playlist_delete_has_node_player_groups": "La playlist è collegata ad un playgroup",
|
||||
"playlist_cast_warning": "Il tuo <a href=\"%href%\" target=\"_blank\">URL esterno</a> deve essere servito in https affinché funzioni",
|
||||
"fleet_node_player_page_title": "Schermi",
|
||||
"fleet_node_player_button_add": "Aggiungi allo schermo",
|
||||
"fleet_node_player_panel_active": "Schermi attivi",
|
||||
@ -185,12 +184,12 @@
|
||||
"settings_variable_desc_auth_enabled": "Abilita la gestione autenticazione",
|
||||
"settings_variable_desc_edition_auth_enabled": "Le credenziali utente predefinite sono %username%/%password%",
|
||||
"settings_variable_desc_external_url": "Url esterno (esempio: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_external_storage_url": "Url esterno per l'archiviazione esterna (esempio: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_slide_upload_limit": "Limite upload slide (in megabytes)",
|
||||
"settings_variable_desc_dark_mode": "Modalità scura",
|
||||
"settings_variable_desc_intro_slide_duration": "Durata introduzione slide (in secondi)",
|
||||
"settings_variable_desc_default_slide_time_with_seconds": "Mostra secondi introduzione slide",
|
||||
"settings_variable_desc_polling_interval": "Intervallo di aggiornamento applicato per le impostazioni del monitor (in secondi)",
|
||||
"settings_variable_desc_player_content_cache": "Abilita la cache",
|
||||
"settings_variable_desc_slide_animation_enabled": "Abilita l'effetto di animazione tra le diapositive",
|
||||
"settings_variable_desc_slide_animation_entrance_effect": "Effetto ingresso diapositiva",
|
||||
"settings_variable_desc_slide_animation_exit_effect": "Effetto di uscita della diapositiva (meglio senza)",
|
||||
@ -238,7 +237,6 @@
|
||||
"common_pick_element": "Scegli un elemento",
|
||||
"common_untitled": "<senza-titolo>",
|
||||
"common_loading": "Caricamento...",
|
||||
"common_casting": "Casting...",
|
||||
"common_default_node_player_group": "Playgroup di default",
|
||||
"common_default_playlist": "Default playlist",
|
||||
"common_unknown_ipaddr": "IP sconosciuto",
|
||||
@ -256,27 +254,12 @@
|
||||
"common_apply": "Applica",
|
||||
"common_saved": "Le modifiche sono state salvate",
|
||||
"common_new_folder": "Nuovo Cartella",
|
||||
"common_folder_not_empty_error": "La cartella '%folderName%' non è vuota, devi prima eliminarne il contenuto",
|
||||
"common_folder_not_empty_error": "La cartella non è vuota, devi prima eliminarne il contenuto",
|
||||
"common_copied": "Elemento copiato!",
|
||||
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Si è verificato un errore %code%",
|
||||
"common_http_error_413": "I file sono troppo grandi",
|
||||
"common_width": "Larghezza",
|
||||
"common_height": "Altezza",
|
||||
"common_position": "Posizione",
|
||||
"common_angle": "Angolo",
|
||||
"common_size": "Dimensioni",
|
||||
"composition_elements_heading": "Elementi",
|
||||
"composition_element_add": "Aggiungi elemento",
|
||||
"composition_elements_delete_all": "Elimina tutto",
|
||||
"composition_presets": "Preimpostazioni",
|
||||
"composition_presets_grid_2x2": "Griglia 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Schermo",
|
||||
"composition_element_x_axis": "Asse X",
|
||||
"composition_element_y_axis": "Asse Y",
|
||||
"composition_element_match_content_aspect_ratio": "Regola la scala del contenuto",
|
||||
"logout": "Logout",
|
||||
"login_error_not_found": "Credenziali errate",
|
||||
"login_error_bad_credentials": "Credenziali errate",
|
||||
@ -308,10 +291,6 @@
|
||||
"enum_content_type_external_storage": "Archiviazione esterna",
|
||||
"enum_content_type_external_storage_object_label": "Specificare una directory esistente relativi al seguente percorso",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Percorso relativo ad un dispositivo rimovibile",
|
||||
"enum_content_type_composition": "Composizione",
|
||||
"enum_content_type_composition_object_label": "Rapporto di aspetto dello schermo",
|
||||
"enum_content_type_text": "Testo",
|
||||
"enum_content_type_text_object_label": "Testo visualizzato",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Immagine",
|
||||
@ -330,8 +309,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Altro",
|
||||
"sysinfo_device_model": "Modello del dispositivo",
|
||||
"sysinfo_device_model_unknown": "Modello sconosciuto",
|
||||
"sysinfo_rpi_model": "Raspberry Pi Model",
|
||||
"sysinfo_rpi_model_unknown": "Informazioni Raspberry Pi non disponibili",
|
||||
"sysinfo_storage_free_space": "Spazio libero",
|
||||
"sysinfo_memory_usage": "Memoria usata",
|
||||
"sysinfo_os_version": "OS Version",
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
from src.interface.ObPlugin import ObPlugin
|
||||
|
||||
from typing import List, Dict
|
||||
from src.model.entity.Variable import Variable
|
||||
from src.model.enum.HookType import HookType
|
||||
from src.model.hook.HookRegistration import HookRegistration
|
||||
|
||||
|
||||
class CoreApi(ObPlugin):
|
||||
|
||||
def get_version(self) -> str:
|
||||
return '1.0'
|
||||
|
||||
def use_id(self):
|
||||
return 'core_api'
|
||||
|
||||
def use_title(self):
|
||||
return self.translate('plugin_title')
|
||||
|
||||
def use_description(self):
|
||||
return self.translate('plugin_description')
|
||||
|
||||
def use_help_on_activation(self):
|
||||
return self.translate('plugin_help_on_activation')
|
||||
|
||||
def use_variables(self) -> List[Variable]:
|
||||
return []
|
||||
|
||||
def use_hooks_registrations(self) -> List[HookRegistration]:
|
||||
return []
|
||||
@ -1,375 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields, reqparse
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.model.entity.Content import Content
|
||||
from src.manager.FolderManager import FolderManager
|
||||
from src.model.enum.ContentType import ContentType, ContentInputType
|
||||
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import str_to_enum
|
||||
from plugins.system.CoreApi.exception.ContentPathMissingException import ContentPathMissingException
|
||||
from plugins.system.CoreApi.exception.ContentNotFoundException import ContentNotFoundException
|
||||
from plugins.system.CoreApi.exception.FolderNotFoundException import FolderNotFoundException
|
||||
from plugins.system.CoreApi.exception.FolderNotEmptyException import FolderNotEmptyException
|
||||
from src.service.WebServer import create_require_api_key_decorator
|
||||
|
||||
|
||||
# Namespace for content operations
|
||||
content_ns = Namespace('contents', description='Operations on contents')
|
||||
|
||||
# Output model for content
|
||||
content_output_model = content_ns.model('ContentOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='Unique identifier of the content'),
|
||||
'name': fields.String(description='Name of the content'),
|
||||
'type': fields.String(description='Type of the content'),
|
||||
'location': fields.String(description='Location of the content'),
|
||||
'folder_id': fields.Integer(description='Folder ID where the content is stored')
|
||||
})
|
||||
|
||||
# Model for folder operations
|
||||
folder_model = content_ns.model('Folder', {
|
||||
'name': fields.String(required=True, description='Name of the folder'),
|
||||
'path': fields.String(required=False, description='Path context (with path starting with /)'),
|
||||
'folder_id': fields.Integer(required=False, description='Path context (with folder id)')
|
||||
})
|
||||
|
||||
# Parser for bulk move operations
|
||||
bulk_move_parser = content_ns.parser()
|
||||
bulk_move_parser.add_argument('entity_ids', type=int, action='append', required=True, help='List of content IDs to move')
|
||||
bulk_move_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
bulk_move_parser.add_argument('folder_id', type=int, required=False, help='Path context (with folder id)')
|
||||
|
||||
# Parser for content add/upload (single file)
|
||||
content_upload_parser = content_ns.parser()
|
||||
content_upload_parser.add_argument('name', type=str, required=True, help='Name of the content')
|
||||
content_upload_parser.add_argument('type', type=str, required=True, help='Type of the content')
|
||||
content_upload_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
content_upload_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)')
|
||||
content_upload_parser.add_argument('location', type=str, required=False, help="Content location (valid for types: {}, {} and {})".format(
|
||||
ContentType.URL.value,
|
||||
ContentType.YOUTUBE.value,
|
||||
ContentType.EXTERNAL_STORAGE.value
|
||||
))
|
||||
content_upload_parser.add_argument('object', type=FileStorage, location='files', required=False, help="Content location (valid for types: {} and {})".format(
|
||||
ContentType.PICTURE.value,
|
||||
ContentType.VIDEO.value
|
||||
))
|
||||
|
||||
# Parser for content add/bulk uploads (multiple files)
|
||||
bulk_upload_parser = content_ns.parser()
|
||||
bulk_upload_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
bulk_upload_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)')
|
||||
bulk_upload_parser.add_argument('object', type=FileStorage, location='files', action='append', required=True, help='Files to be uploaded')
|
||||
|
||||
# Parser for content edit
|
||||
content_edit_parser = content_ns.parser()
|
||||
content_edit_parser.add_argument('name', type=str, required=True, help='Name of the content')
|
||||
|
||||
# Parser for content path context actions
|
||||
path_parser = content_ns.parser()
|
||||
path_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
path_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)')
|
||||
|
||||
# Parser for folder add/edit
|
||||
folder_parser = content_ns.parser()
|
||||
folder_parser.add_argument('name', type=str, required=True, help='Name of the folder')
|
||||
folder_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
folder_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)')
|
||||
|
||||
class ContentApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self.api().add_namespace(content_ns, path='/api/contents')
|
||||
content_ns.add_resource(self.create_resource(ContentListResource), '/')
|
||||
content_ns.add_resource(self.create_resource(ContentResource), '/<int:content_id>')
|
||||
content_ns.add_resource(self.create_resource(ContentLocationResource), '/location/<int:content_id>')
|
||||
content_ns.add_resource(self.create_resource(ContentBulkUploadResource), '/upload-bulk')
|
||||
content_ns.add_resource(self.create_resource(FolderBulkMoveResource), '/folder/move-bulk')
|
||||
content_ns.add_resource(self.create_resource(FolderResource), '/folder')
|
||||
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store,
|
||||
'_controller': self,
|
||||
'require_api_key': create_require_api_key_decorator(self._web_server)
|
||||
})
|
||||
|
||||
def _get_folder_context(self, data):
|
||||
path = data.get('path', None)
|
||||
folder_id = data.get('folder_id', None)
|
||||
|
||||
if folder_id:
|
||||
folder = self._model_store.folder().get(id=folder_id)
|
||||
|
||||
if not folder:
|
||||
raise FolderNotFoundException()
|
||||
return path, folder
|
||||
|
||||
if not path:
|
||||
raise ContentPathMissingException()
|
||||
|
||||
path = "{}/{}".format(FOLDER_ROOT_PATH, path.strip('/')) if not path.startswith(FOLDER_ROOT_PATH) else path
|
||||
|
||||
folder = self._model_store.folder().get_one_by_path(path=path, entity=FolderEntity.CONTENT)
|
||||
is_root_drive = FolderManager.is_root_drive(path)
|
||||
|
||||
if not folder and not is_root_drive:
|
||||
raise FolderNotFoundException()
|
||||
|
||||
return FOLDER_ROOT_PATH if is_root_drive else path, folder
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_content_update", time.time())
|
||||
|
||||
|
||||
class ContentListResource(Resource):
|
||||
|
||||
@content_ns.expect(path_parser)
|
||||
@content_ns.marshal_list_with(content_output_model)
|
||||
def get(self):
|
||||
"""List all contents"""
|
||||
self.require_api_key()
|
||||
data = path_parser.parse_args()
|
||||
working_folder_path = None
|
||||
working_folder = None
|
||||
folder_id = None
|
||||
|
||||
try:
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
folder_id = data.get('folder_id', 0 if not working_folder else working_folder.id)
|
||||
except FolderNotFoundException:
|
||||
pass
|
||||
except ContentPathMissingException:
|
||||
pass
|
||||
|
||||
contents = self._model_store.content().get_contents(
|
||||
folder_id=folder_id,
|
||||
slide_id=data.get('slide_id', None),
|
||||
)
|
||||
result = [content.to_dict() for content in contents]
|
||||
|
||||
return result
|
||||
|
||||
@content_ns.expect(content_upload_parser)
|
||||
@content_ns.marshal_with(content_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add new content"""
|
||||
self.require_api_key()
|
||||
data = content_upload_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
location = data.get('location', None)
|
||||
content_type = None
|
||||
|
||||
# Handle content type conversion
|
||||
try:
|
||||
content_type = str_to_enum(data.get('type'), ContentType)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
# Handle file upload
|
||||
file = data.get('object', None)
|
||||
|
||||
if ContentType.get_input(content_type) == ContentInputType.UPLOAD:
|
||||
if not file:
|
||||
abort(400, description="File is required")
|
||||
|
||||
content = self._model_store.content().add_form_raw(
|
||||
name=data.get('name'),
|
||||
type=content_type,
|
||||
request_files=file,
|
||||
upload_dir=self._controller._app.config['UPLOAD_FOLDER'],
|
||||
location=location,
|
||||
folder_id=working_folder.id if working_folder else None
|
||||
)
|
||||
|
||||
if not content:
|
||||
abort(400, description="Failed to add content")
|
||||
|
||||
return content.to_dict(), 201
|
||||
|
||||
|
||||
class ContentResource(Resource):
|
||||
|
||||
@content_ns.marshal_with(content_output_model)
|
||||
def get(self, content_id: int):
|
||||
"""Get content by ID"""
|
||||
self.require_api_key()
|
||||
content = self._model_store.content().get(content_id)
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
return content.to_dict()
|
||||
|
||||
@content_ns.expect(content_edit_parser)
|
||||
@content_ns.marshal_with(content_output_model)
|
||||
def put(self, content_id: int):
|
||||
"""Update existing content"""
|
||||
self.require_api_key()
|
||||
data = content_edit_parser.parse_args()
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
|
||||
content = self._model_store.content().update_form(
|
||||
id=content.id,
|
||||
name=data.get('name'),
|
||||
)
|
||||
|
||||
self._controller._post_update()
|
||||
|
||||
return content.to_dict()
|
||||
|
||||
def delete(self, content_id: int):
|
||||
"""Delete content"""
|
||||
self.require_api_key()
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
if self._model_store.slide().count_slides_for_content(content.id) > 0:
|
||||
abort(400, description="Content is referenced in slides")
|
||||
|
||||
self._model_store.content().delete(content.id)
|
||||
self._controller._post_update()
|
||||
|
||||
return {'status': 'ok'}, 204
|
||||
|
||||
|
||||
class ContentLocationResource(Resource):
|
||||
|
||||
def get(self, content_id: int):
|
||||
"""Get content location by ID"""
|
||||
self.require_api_key()
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
content_location = self._model_store.content().resolve_content_location(content)
|
||||
|
||||
return {'location': content_location}
|
||||
|
||||
|
||||
class ContentBulkUploadResource(Resource):
|
||||
|
||||
@content_ns.expect(bulk_upload_parser)
|
||||
def post(self):
|
||||
"""Upload multiple content files"""
|
||||
self.require_api_key()
|
||||
data = bulk_upload_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
for file in data.get('object'):
|
||||
content_type = ContentType.guess_content_type_file(file.filename)
|
||||
name = file.filename.rsplit('.', 1)[0]
|
||||
|
||||
if content_type:
|
||||
self._model_store.content().add_form_raw(
|
||||
name=name,
|
||||
type=content_type,
|
||||
request_files=file,
|
||||
upload_dir=self._controller._app.config['UPLOAD_FOLDER'],
|
||||
folder_id=working_folder.id if working_folder else None
|
||||
)
|
||||
|
||||
return {'status': 'ok'}, 201
|
||||
|
||||
|
||||
class FolderBulkMoveResource(Resource):
|
||||
|
||||
@content_ns.expect(bulk_move_parser)
|
||||
def post(self):
|
||||
"""Move multiple content to another folder"""
|
||||
self.require_api_key()
|
||||
data = bulk_move_parser.parse_args()
|
||||
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
if 'entity_ids' not in data:
|
||||
abort(400, description="Content IDs are required under 'entity_ids' field")
|
||||
|
||||
entity_ids = data.get('entity_ids')
|
||||
|
||||
for entity_id in entity_ids:
|
||||
self._model_store.folder().move_to_folder(
|
||||
entity_id=entity_id,
|
||||
folder_id=working_folder.id if working_folder else None,
|
||||
entity_is_folder=False,
|
||||
entity=FolderEntity.CONTENT
|
||||
)
|
||||
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
class FolderResource(Resource):
|
||||
|
||||
@content_ns.expect(folder_parser)
|
||||
@content_ns.marshal_with(folder_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new folder"""
|
||||
self.require_api_key()
|
||||
data = folder_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
|
||||
folder = self._model_store.folder().add_folder(
|
||||
entity=FolderEntity.CONTENT,
|
||||
name=data.get('name'),
|
||||
working_folder_path=working_folder_path
|
||||
)
|
||||
|
||||
return folder.to_dict(), 201
|
||||
|
||||
@content_ns.expect(path_parser)
|
||||
def delete(self):
|
||||
"""Delete a folder"""
|
||||
self.require_api_key()
|
||||
data = path_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
if not working_folder:
|
||||
abort(400, description="You can't delete this folder")
|
||||
|
||||
content_counter = self._model_store.content().count_contents_for_folder(working_folder.id)
|
||||
folder_counter = self._model_store.folder().count_subfolders_for_folder(working_folder.id)
|
||||
|
||||
if content_counter > 0 or folder_counter:
|
||||
raise FolderNotEmptyException()
|
||||
|
||||
self._model_store.folder().delete(id=working_folder.id)
|
||||
self._controller._post_update()
|
||||
|
||||
return {'status': 'ok'}, 204
|
||||
|
||||
@content_ns.expect(folder_parser)
|
||||
def put(self):
|
||||
"""Update a folder"""
|
||||
self.require_api_key()
|
||||
data = folder_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
|
||||
if not working_folder:
|
||||
abort(400, description="You can't update this folder")
|
||||
|
||||
self._model_store.folder().rename_folder(
|
||||
folder_id=working_folder.id,
|
||||
name=data.get('name')
|
||||
)
|
||||
|
||||
return {'status': 'ok'}
|
||||
@ -1,161 +0,0 @@
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import str_to_bool
|
||||
from src.service.WebServer import create_require_api_key_decorator
|
||||
|
||||
|
||||
# Namespace for playlists operations
|
||||
playlist_ns = Namespace('playlists', description='Operations on playlist')
|
||||
|
||||
# Output model for a playlist
|
||||
playlist_output_model = playlist_ns.model('PlaylistOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='The unique identifier of a playlist'),
|
||||
'name': fields.String(required=True, description='The playlist name'),
|
||||
'enabled': fields.Boolean(description='Is the playlist enabled?'),
|
||||
'time_sync': fields.Boolean(description='Is time synchronization enabled?')
|
||||
})
|
||||
|
||||
# Parser for playlist attributes (add)
|
||||
playlist_parser = playlist_ns.parser()
|
||||
playlist_parser.add_argument('name', type=str, required=True, help='The playlist name')
|
||||
playlist_parser.add_argument('enabled', type=str_to_bool, default=None, help='Is the playlist enabled?')
|
||||
playlist_parser.add_argument('time_sync', type=str_to_bool, default=None, help='Is time synchronization enabled for slideshow?')
|
||||
|
||||
# Parser for playlist attributes (update)
|
||||
playlist_edit_parser = playlist_parser.copy()
|
||||
playlist_edit_parser.replace_argument('name', type=str, required=False, help='The playlist name')
|
||||
|
||||
|
||||
class PlaylistApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self.api().add_namespace(playlist_ns, path='/api/playlists')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistResource), '/<int:playlist_id>')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistListResource), '/')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistSlidesResource), '/<int:playlist_id>/slides')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistNotificationsResource), '/<int:playlist_id>/notifications')
|
||||
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store,
|
||||
'_controller': self,
|
||||
'require_api_key': create_require_api_key_decorator(self._web_server)
|
||||
})
|
||||
|
||||
|
||||
class PlaylistListResource(Resource):
|
||||
|
||||
@playlist_ns.marshal_list_with(playlist_output_model)
|
||||
def get(self):
|
||||
"""List all playlists"""
|
||||
self.require_api_key()
|
||||
playlists = self._model_store.playlist().get_all(sort="created_at", ascending=True)
|
||||
result = [playlist.to_dict() for playlist in playlists]
|
||||
return result
|
||||
|
||||
@playlist_ns.expect(playlist_parser)
|
||||
@playlist_ns.marshal_with(playlist_output_model, code=201)
|
||||
def post(self):
|
||||
"""Create a new playlist"""
|
||||
self.require_api_key()
|
||||
data = playlist_parser.parse_args()
|
||||
|
||||
if not data.get('name'):
|
||||
abort(400, description="Invalid input")
|
||||
|
||||
playlist = Playlist(
|
||||
name=data.get('name'),
|
||||
enabled=data.get('enabled') if data.get('enabled') is not None else True,
|
||||
time_sync=data.get('time_sync') if data.get('time_sync') is not None else False,
|
||||
)
|
||||
|
||||
try:
|
||||
playlist = self._model_store.playlist().add_form(playlist)
|
||||
except Exception as e:
|
||||
abort(409, description=str(e))
|
||||
|
||||
return playlist.to_dict(), 201
|
||||
|
||||
|
||||
class PlaylistResource(Resource):
|
||||
|
||||
@playlist_ns.marshal_with(playlist_output_model)
|
||||
def get(self, playlist_id):
|
||||
"""Get a playlist by its ID"""
|
||||
self.require_api_key()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
return playlist.to_dict()
|
||||
|
||||
@playlist_ns.expect(playlist_edit_parser)
|
||||
@playlist_ns.marshal_with(playlist_output_model)
|
||||
def put(self, playlist_id):
|
||||
"""Update an existing playlist"""
|
||||
self.require_api_key()
|
||||
data = playlist_edit_parser.parse_args()
|
||||
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
self._model_store.playlist().update_form(
|
||||
id=playlist_id,
|
||||
name=data.get('name', playlist.name),
|
||||
time_sync=data.get('time_sync', playlist.time_sync),
|
||||
enabled=data.get('enabled', playlist.enabled)
|
||||
)
|
||||
updated_playlist = self._model_store.playlist().get(playlist_id)
|
||||
return updated_playlist.to_dict()
|
||||
|
||||
def delete(self, playlist_id):
|
||||
"""Delete a playlist"""
|
||||
self.require_api_key()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
if self._model_store.slide().count_slides_for_playlist(playlist_id) > 0:
|
||||
abort(400, description="Playlist cannot be deleted because it has slides")
|
||||
|
||||
if self._model_store.node_player_group().count_node_player_groups_for_playlist(playlist_id) > 0:
|
||||
abort(400, description="Playlist cannot be deleted because it is associated with node player groups")
|
||||
|
||||
self._model_store.playlist().delete(playlist_id)
|
||||
return '', 204
|
||||
|
||||
|
||||
class PlaylistSlidesResource(Resource):
|
||||
|
||||
def get(self, playlist_id):
|
||||
"""Get slides associated with a playlist"""
|
||||
self.require_api_key()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
slides = self._model_store.slide().get_slides(is_notification=False, playlist_id=playlist_id)
|
||||
|
||||
result = [slide.to_dict() for slide in slides]
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
class PlaylistNotificationsResource(Resource):
|
||||
|
||||
def get(self, playlist_id):
|
||||
"""Get notifications associated with a playlist"""
|
||||
self.require_api_key()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
slides = self._model_store.slide().get_slides(is_notification=True, playlist_id=playlist_id)
|
||||
|
||||
result = [slide.to_dict() for slide in slides]
|
||||
return jsonify(result)
|
||||
@ -1,322 +0,0 @@
|
||||
import time
|
||||
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
from src.model.entity.Slide import Slide
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import str_datetime_to_cron, str_weekdaytime_to_cron, str_to_bool
|
||||
from src.service.WebServer import create_require_api_key_decorator
|
||||
|
||||
# Namespace for slide operations
|
||||
slide_ns = Namespace('slides', description='Operations on slides')
|
||||
|
||||
# Output model for a slide
|
||||
slide_output_model = slide_ns.model('SlideOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='The unique identifier of a slide'),
|
||||
'content_id': fields.Integer(description='The content ID for the slide'),
|
||||
'playlist_id': fields.Integer(description='The playlist ID to which the slide belongs'),
|
||||
'enabled': fields.Boolean(description='Is the slide enabled?'),
|
||||
'delegate_duration': fields.Boolean(description='Should the duration be delegated?'),
|
||||
'duration': fields.Integer(description='Duration of the slide'),
|
||||
'position': fields.Integer(description='Position of the slide'),
|
||||
'is_notification': fields.Boolean(description='Is the slide a notification?'),
|
||||
'cron_schedule': fields.String(description='Cron expression for scheduling start'),
|
||||
'cron_schedule_end': fields.String(description='Cron expression for scheduling end'),
|
||||
})
|
||||
|
||||
# Input model for updating slide positions
|
||||
positions_model = slide_ns.model('SlidePositions', {
|
||||
'positions': fields.Raw(required=True, description='A dictionary where keys are slide IDs and values are their new positions')
|
||||
})
|
||||
|
||||
# Parser for basic slide attributes
|
||||
slide_base_parser = slide_ns.parser()
|
||||
slide_base_parser.add_argument('content_id', type=int, required=True, help='The content ID for the slide')
|
||||
slide_base_parser.add_argument('playlist_id', type=int, required=True, help='The playlist ID to which the slide belongs')
|
||||
slide_base_parser.add_argument('enabled', type=str_to_bool, default=None, help='Is the slide enabled?')
|
||||
slide_base_parser.add_argument('duration', type=int, default=3, help='Duration of the slide')
|
||||
slide_base_parser.add_argument('position', type=int, default=999, help='Position of the slide')
|
||||
|
||||
# Parser for slide attributes (add)
|
||||
slide_parser = slide_base_parser.copy()
|
||||
slide_parser.add_argument('scheduling', type=str, required=True, help='Scheduling type: loop, datetime or inweek')
|
||||
slide_parser.add_argument('delegate_duration', type=str_to_bool, default=None, help='Should the duration be delegated to video\'s duration?')
|
||||
slide_parser.add_argument('datetime_start', type=str, required=False, help='Start datetime for scheduling (format: Y-m-d H:M)')
|
||||
slide_parser.add_argument('datetime_end', type=str, required=False, help='End datetime for scheduling (format: Y-m-d H:M)')
|
||||
slide_parser.add_argument('day_start', type=int, required=False, help='Start day for inweek scheduling (format: 1 for Monday to 7 for Sunday)')
|
||||
slide_parser.add_argument('time_start', type=str, required=False, help='Start time for inweek scheduling (format: H:M)')
|
||||
slide_parser.add_argument('day_end', type=int, required=False, help='End day for inweek scheduling (format: 1 for Monday to 7 for Sunday)')
|
||||
slide_parser.add_argument('time_end', type=str, required=False, help='End time for inweek scheduling (format: H:M)')
|
||||
|
||||
# Parser for slide notification attributes (add)
|
||||
slide_notification_parser = slide_base_parser.copy()
|
||||
slide_notification_parser.add_argument('scheduling', type=str, required=True, help='Scheduling type: datetime or cron')
|
||||
slide_notification_parser.add_argument('datetime_start', type=str, required=False, help='Start datetime for notification scheduling (format: Y-m-d H:M)')
|
||||
slide_notification_parser.add_argument('datetime_end', type=str, required=False, help='End datetime for notification scheduling (format: Y-m-d H:M)')
|
||||
slide_notification_parser.add_argument('cron_start', type=str, required=False, help='Cron expression for notification scheduling start (format: * * * * * * *)')
|
||||
slide_notification_parser.add_argument('cron_end', type=str, required=False, help='Cron expression for notification scheduling end (format: * * * * * * *)')
|
||||
|
||||
# Parser for slide attributes (update)
|
||||
slide_edit_parser = slide_parser.copy()
|
||||
slide_edit_parser.replace_argument('scheduling', type=str, required=False, help='Scheduling type: loop, datetime, or inweek')
|
||||
slide_edit_parser.replace_argument('content_id', type=int, required=False, help='The content ID for the slide')
|
||||
slide_edit_parser.replace_argument('playlist_id', type=int, required=False, help='The playlist ID to which the slide belongs')
|
||||
|
||||
# Parser for slide notification attributes (update)
|
||||
slide_notification_edit_parser = slide_notification_parser.copy()
|
||||
slide_notification_edit_parser.replace_argument('scheduling', type=str, required=False, help='Scheduling type: datetime or cron')
|
||||
slide_notification_edit_parser.replace_argument('content_id', type=int, required=False, help='The content ID for the slide')
|
||||
slide_notification_edit_parser.replace_argument('playlist_id', type=int, required=False, help='The playlist ID to which the slide belongs')
|
||||
|
||||
|
||||
class SlideApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self.api().add_namespace(slide_ns, path='/api/slides')
|
||||
slide_ns.add_resource(self.create_resource(SlideNotificationResource), '/notifications/<int:slide_id>')
|
||||
slide_ns.add_resource(self.create_resource(SlideResource), '/<int:slide_id>')
|
||||
slide_ns.add_resource(self.create_resource(SlideAddResource), '/')
|
||||
slide_ns.add_resource(self.create_resource(SlideAddNotificationResource), '/notifications')
|
||||
slide_ns.add_resource(self.create_resource(SlidePositionResource), '/positions')
|
||||
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store,
|
||||
'_controller': self,
|
||||
'require_api_key': create_require_api_key_decorator(self._web_server)
|
||||
})
|
||||
|
||||
def _add_slide_or_notification(self, data, is_notification=False):
|
||||
if not data or 'content_id' not in data:
|
||||
abort(400, description="Valid Content ID is required")
|
||||
|
||||
if not self._model_store.content().get(data.get('content_id')):
|
||||
abort(404, description="Content not found")
|
||||
|
||||
if not data or 'playlist_id' not in data:
|
||||
abort(400, description="Valid Playlist ID is required")
|
||||
|
||||
if not self._model_store.playlist().get(data.get('playlist_id')):
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
cron_schedule_start, cron_schedule_end = self._resolve_scheduling(data, is_notification=is_notification)
|
||||
|
||||
slide = Slide(
|
||||
content_id=data.get('content_id'),
|
||||
enabled=data.get('enabled') if data.get('enabled') is not None else True,
|
||||
delegate_duration=data.get('delegate_duration') if data.get('delegate_duration') is not None else False,
|
||||
duration=data.get('duration', 3),
|
||||
position=data.get('position', 999),
|
||||
is_notification=is_notification,
|
||||
playlist_id=data.get('playlist_id', None),
|
||||
cron_schedule=cron_schedule_start,
|
||||
cron_schedule_end=cron_schedule_end
|
||||
)
|
||||
|
||||
slide = self._model_store.slide().add_form(slide)
|
||||
self._post_update()
|
||||
|
||||
return slide.to_dict(), 201
|
||||
|
||||
def _resolve_scheduling(self, data, is_notification=False):
|
||||
try:
|
||||
return self._resolve_scheduling_for_notification(data) if is_notification else self._resolve_scheduling_for_slide(data)
|
||||
except ValueError as ve:
|
||||
abort(400, description=str(ve))
|
||||
|
||||
def _resolve_scheduling_for_slide(self, data):
|
||||
scheduling = data.get('scheduling', 'loop')
|
||||
cron_schedule_start = None
|
||||
cron_schedule_end = None
|
||||
|
||||
if scheduling == 'loop':
|
||||
pass
|
||||
elif scheduling == 'datetime':
|
||||
datetime_start = data.get('datetime_start')
|
||||
datetime_end = data.get('datetime_end')
|
||||
|
||||
if not datetime_start:
|
||||
abort(400, description="Field datetime_start is required for scheduling='datetime'")
|
||||
|
||||
cron_schedule_start = str_datetime_to_cron(datetime_str=datetime_start)
|
||||
|
||||
if datetime_end:
|
||||
cron_schedule_end = str_datetime_to_cron(datetime_str=datetime_end)
|
||||
elif scheduling == 'inweek':
|
||||
day_start = data.get('day_start')
|
||||
time_start = data.get('time_start')
|
||||
day_end = data.get('day_end')
|
||||
time_end = data.get('time_end')
|
||||
|
||||
if not (day_start and time_start and day_end and time_end):
|
||||
abort(400, description="day_start, time_start, day_end, and time_end are required for scheduling='inweek'")
|
||||
cron_schedule_start = str_weekdaytime_to_cron(weekday=int(day_start), time_str=time_start)
|
||||
cron_schedule_end = str_weekdaytime_to_cron(weekday=int(day_end), time_str=time_end)
|
||||
else:
|
||||
abort(400, description="Invalid value for slide scheduling. Expected 'loop', 'datetime', or 'inweek'.")
|
||||
|
||||
return cron_schedule_start, cron_schedule_end
|
||||
|
||||
def _resolve_scheduling_for_notification(self, data):
|
||||
scheduling = data.get('scheduling', 'datetime')
|
||||
cron_schedule_start = None
|
||||
cron_schedule_end = None
|
||||
|
||||
if scheduling == 'datetime':
|
||||
datetime_start = data.get('datetime_start')
|
||||
datetime_end = data.get('datetime_end')
|
||||
|
||||
if not datetime_start:
|
||||
abort(400, description="Field datetime_start is required for scheduling='datetime'")
|
||||
|
||||
cron_schedule_start = str_datetime_to_cron(datetime_str=datetime_start)
|
||||
|
||||
if datetime_end:
|
||||
cron_schedule_end = str_datetime_to_cron(datetime_str=datetime_end)
|
||||
elif scheduling == 'cron':
|
||||
cron_schedule_start = data.get('cron_start')
|
||||
|
||||
if not cron_schedule_start:
|
||||
abort(400, description="Field cron_start is required for scheduling='cron'")
|
||||
else:
|
||||
abort(400, description="Invalid value for notification scheduling. Expected 'datetime' or 'cron'.")
|
||||
|
||||
return cron_schedule_start, cron_schedule_end
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
||||
|
||||
|
||||
class SlideAddResource(Resource):
|
||||
|
||||
@slide_ns.expect(slide_parser)
|
||||
@slide_ns.marshal_with(slide_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new slide"""
|
||||
self.require_api_key()
|
||||
data = slide_parser.parse_args()
|
||||
return self._controller._add_slide_or_notification(data, is_notification=False)
|
||||
|
||||
|
||||
class SlideAddNotificationResource(Resource):
|
||||
|
||||
@slide_ns.expect(slide_notification_parser)
|
||||
@slide_ns.marshal_with(slide_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new slide notification"""
|
||||
self.require_api_key()
|
||||
data = slide_notification_parser.parse_args()
|
||||
return self._controller._add_slide_or_notification(data, is_notification=True)
|
||||
|
||||
|
||||
class SlideResource(Resource):
|
||||
|
||||
@slide_ns.marshal_with(slide_output_model)
|
||||
def get(self, slide_id):
|
||||
"""Get a slide by its ID"""
|
||||
self.require_api_key()
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
return slide.to_dict()
|
||||
|
||||
@slide_ns.expect(slide_edit_parser)
|
||||
@slide_ns.marshal_with(slide_output_model)
|
||||
def put(self, slide_id):
|
||||
"""Edit an existing slide"""
|
||||
self.require_api_key()
|
||||
data = slide_edit_parser.parse_args()
|
||||
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
cron_schedule_start = slide.cron_schedule
|
||||
cron_schedule_end = slide.cron_schedule_end
|
||||
|
||||
if data.get('scheduling'):
|
||||
cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification)
|
||||
|
||||
self._model_store.slide().update_form(
|
||||
id=slide_id,
|
||||
content_id=data.get('content_id', slide.content_id),
|
||||
enabled=data.get('enabled', slide.enabled),
|
||||
position=data.get('position', slide.position),
|
||||
duration=data.get('duration', slide.duration),
|
||||
cron_schedule=cron_schedule_start,
|
||||
cron_schedule_end=cron_schedule_end
|
||||
)
|
||||
self._controller._post_update()
|
||||
|
||||
updated_slide = self._model_store.slide().get(slide_id)
|
||||
return updated_slide.to_dict()
|
||||
|
||||
def delete(self, slide_id):
|
||||
"""Delete a slide"""
|
||||
self.require_api_key()
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
self._model_store.slide().delete(slide_id)
|
||||
self._controller._post_update()
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
class SlideNotificationResource(Resource):
|
||||
|
||||
@slide_ns.expect(slide_notification_edit_parser)
|
||||
@slide_ns.marshal_with(slide_output_model)
|
||||
def put(self, slide_id):
|
||||
"""Edit an existing slide notification"""
|
||||
self.require_api_key()
|
||||
data = slide_notification_edit_parser.parse_args()
|
||||
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
cron_schedule_start = slide.cron_schedule
|
||||
cron_schedule_end = slide.cron_schedule_end
|
||||
|
||||
if data.get('scheduling'):
|
||||
cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification)
|
||||
|
||||
self._model_store.slide().update_form(
|
||||
id=slide_id,
|
||||
content_id=data.get('content_id', slide.content_id),
|
||||
enabled=data.get('enabled', slide.enabled),
|
||||
position=data.get('position', slide.position),
|
||||
delegate_duration=data.get('delegate_duration', slide.delegate_duration),
|
||||
duration=data.get('duration', slide.duration),
|
||||
cron_schedule=cron_schedule_start,
|
||||
cron_schedule_end=cron_schedule_end
|
||||
)
|
||||
self._controller._post_update()
|
||||
|
||||
updated_slide = self._model_store.slide().get(slide_id)
|
||||
return updated_slide.to_dict()
|
||||
|
||||
|
||||
class SlidePositionResource(Resource):
|
||||
|
||||
@slide_ns.expect(positions_model)
|
||||
def post(self):
|
||||
"""Update positions of multiple slides"""
|
||||
self.require_api_key()
|
||||
data = request.get_json()
|
||||
positions = data.get('positions', None) if data else None
|
||||
|
||||
if not positions:
|
||||
abort(400, description="Positions data are required")
|
||||
|
||||
# Ensure the input is a dictionary with integer keys and values
|
||||
if not isinstance(data, dict) or not all(isinstance(k, str) and isinstance(v, int) for k, v in positions.items()):
|
||||
abort(400, description="Input must be a dictionary with string keys as slide IDs and integer values as positions")
|
||||
|
||||
self._model_store.slide().update_positions(positions)
|
||||
self._controller._post_update()
|
||||
return jsonify({'status': 'ok'})
|
||||
@ -1,6 +0,0 @@
|
||||
from src.exceptions.HttpClientException import HttpClientException
|
||||
|
||||
|
||||
class ContentNotFoundException(HttpClientException):
|
||||
code = 404
|
||||
description = "Content not found"
|
||||
@ -1,6 +0,0 @@
|
||||
from src.exceptions.HttpClientException import HttpClientException
|
||||
|
||||
|
||||
class ContentPathMissingException(HttpClientException):
|
||||
code = 400
|
||||
description = "Path is required"
|
||||
@ -1,6 +0,0 @@
|
||||
from src.exceptions.HttpClientException import HttpClientException
|
||||
|
||||
|
||||
class FolderNotEmptyException(HttpClientException):
|
||||
code = 400
|
||||
description = "Folder is not empty"
|
||||
@ -1,6 +0,0 @@
|
||||
from src.exceptions.HttpClientException import HttpClientException
|
||||
|
||||
|
||||
class FolderNotFoundException(HttpClientException):
|
||||
code = 404
|
||||
description = "Folder not found"
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"plugin_title": "Core API",
|
||||
"plugin_description": "Adds api feature wrapping core features",
|
||||
"plugin_help_on_activation": "Documentation will be available on the /api page"
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"plugin_title": "Core API",
|
||||
"plugin_description": "Agrega características de API que envuelven las características principales",
|
||||
"plugin_help_on_activation": "La documentación estará disponible en la página /api"
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"plugin_title": "Core API",
|
||||
"plugin_description": "Ajoute des fonctionnalités d'API englobant les fonctionnalités principales",
|
||||
"plugin_help_on_activation": "La documentation sera disponible sur la page /api"
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"plugin_title": "Core API",
|
||||
"plugin_description": "Aggiunge funzionalità API che racchiudono le funzionalità di base",
|
||||
"plugin_help_on_activation": "La documentazione sarà disponibile nella pagina /api"
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
{% if not am_i_in_docker() %}
|
||||
<a href="{{ url_for('core_updater_update_now') }}" class="btn sysinfo-update protected"><i class="fa fa-cloud-arrow-down icon-left"></i>{{ l.core_updater_button_update }}</a>
|
||||
{% endif %}
|
||||
|
||||
@ -10,11 +10,8 @@ from src.util.utils import am_i_in_docker
|
||||
|
||||
class GitUpdater(ObPlugin):
|
||||
|
||||
def get_version(self) -> str:
|
||||
return '1.0'
|
||||
|
||||
def use_id(self):
|
||||
return 'core_updater'
|
||||
return 'git_updater'
|
||||
|
||||
def use_title(self):
|
||||
return self.translate('plugin_title')
|
||||
@ -22,9 +19,6 @@ class GitUpdater(ObPlugin):
|
||||
def use_description(self):
|
||||
return self.translate('plugin_description')
|
||||
|
||||
def use_help_on_activation(self):
|
||||
return None
|
||||
|
||||
def use_variables(self) -> List[Variable]:
|
||||
return []
|
||||
|
||||
@ -10,10 +10,10 @@ from src.util.utils import run_system_command, sudo_run_system_command, get_work
|
||||
from src.Application import Application
|
||||
|
||||
|
||||
class CoreUpdaterController(ObController):
|
||||
class GitUpdaterController(ObController):
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/core-updater/update/now', 'core_updater_update_now', self._auth(self.update_now), methods=['GET'])
|
||||
self._app.add_url_rule('/git-updater/update/now', 'git_updater_update_now', self._auth(self.update_now), methods=['GET'])
|
||||
|
||||
def update_now(self):
|
||||
debug = self._model_store.config().map().get('debug')
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Core Updater Button",
|
||||
"plugin_title": "Git Updater Button",
|
||||
"plugin_description": "Adds an update button (only for system-wide installations)",
|
||||
"button_update": "Update"
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Core Updater Button",
|
||||
"plugin_title": "Botón de Actualización de Git",
|
||||
"plugin_description": "Añade un botón de actualización (solo para instalaciones a nivel del sistema)",
|
||||
"button_update": "Actualizar"
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Core Updater Button",
|
||||
"plugin_title": "Bouton de mise à jour",
|
||||
"plugin_description": "Ajoute un bouton de mise à jour (seulement pour les installations système)",
|
||||
"button_update": "Mettre à jour"
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Core Updater Button",
|
||||
"plugin_title": "Pulsante di aggiornamento",
|
||||
"plugin_description": "Aggiunge un pulsante di aggiornamento (solo per installazioni di sistema)",
|
||||
"button_update": "Aggiorna"
|
||||
}
|
||||
4
plugins/system/GitUpdater/views/update_button.jinja.html
Normal file
4
plugins/system/GitUpdater/views/update_button.jinja.html
Normal file
@ -0,0 +1,4 @@
|
||||
{% if not am_i_in_docker() %}
|
||||
<a href="{{ url_for('git_updater_update_now') }}" class="btn sysinfo-update protected"><i class="fa fa-cloud-arrow-down icon-left"></i>{{ l.git_updater_button_update }}</a>
|
||||
{% endif %}
|
||||
|
||||
@ -8,9 +8,6 @@ from src.model.hook.HookRegistration import HookRegistration
|
||||
|
||||
class Dashboard(ObPlugin):
|
||||
|
||||
def get_version(self) -> str:
|
||||
return '1.0'
|
||||
|
||||
def use_id(self):
|
||||
return 'dashboard'
|
||||
|
||||
@ -20,9 +17,6 @@ class Dashboard(ObPlugin):
|
||||
def use_description(self):
|
||||
return self.translate('plugin_description')
|
||||
|
||||
def use_help_on_activation(self):
|
||||
return None
|
||||
|
||||
def use_variables(self) -> List[Variable]:
|
||||
return []
|
||||
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block top_page %}
|
||||
{% block page %}
|
||||
|
||||
<div class="top-content">
|
||||
<div class="top-actions">
|
||||
|
||||
@ -31,9 +32,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_page %}
|
||||
<div class="bottom-content">
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
flask==2.3.3
|
||||
flask-restx==1.3.0
|
||||
python-dotenv
|
||||
cron-descriptor
|
||||
waitress
|
||||
flask-login
|
||||
pysqlite3
|
||||
psutil
|
||||
pymediainfo
|
||||
|
||||
56
setup.py
56
setup.py
@ -1,56 +0,0 @@
|
||||
# obscreen
|
||||
# ---------------
|
||||
# A fancy self-hosted digital signage tool. Free, simple and working.
|
||||
#
|
||||
# Author: jr-k (c) 2024
|
||||
# Website: https://github.com/jr-k/obscreen
|
||||
# License: GPLv2 (see LICENSE file)
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
common_dependencies = [
|
||||
'flask==2.3.3',
|
||||
'flask-restx==1.3.0',
|
||||
'python-dotenv',
|
||||
'cron-descriptor',
|
||||
'waitress',
|
||||
'flask-login',
|
||||
'psutil',
|
||||
'pymediainfo',
|
||||
'pysqlite3',
|
||||
]
|
||||
|
||||
if sys.platform == "win32":
|
||||
common_dependencies.remove('pysqlite3')
|
||||
|
||||
if sys.platform == "darwin":
|
||||
common_dependencies.remove('pysqlite3')
|
||||
|
||||
os.environ['PYTHONUTF8'] = '1'
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
|
||||
setup(
|
||||
name='obscreen',
|
||||
version=open('version.txt').read(),
|
||||
description='A fancy self-hosted digital signage tool. Free, simple and working.',
|
||||
long_description=open('README.md').read(),
|
||||
long_description_content_type='text/markdown',
|
||||
author='JRK',
|
||||
author_email='jrk@jierka.com',
|
||||
url='https://github.com/jr-k/obscreen',
|
||||
packages=find_packages(),
|
||||
platforms='any',
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3',
|
||||
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
|
||||
'Operating System :: OS Independent',
|
||||
'Topic :: Desktop Environment :: Screen Savers',
|
||||
'Topic :: Multimedia :: Graphics'
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
install_requires=common_dependencies,
|
||||
)
|
||||
@ -6,6 +6,7 @@ import threading
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.service.PluginStore import PluginStore
|
||||
from src.service.TemplateRenderer import TemplateRenderer
|
||||
from src.service.ExternalStorageServer import ExternalStorageServer
|
||||
from src.service.WebServer import WebServer
|
||||
from src.model.enum.HookType import HookType
|
||||
|
||||
@ -18,6 +19,7 @@ class Application:
|
||||
self._model_store = ModelStore(self, self.get_plugins)
|
||||
self._template_renderer = TemplateRenderer(kernel=self, model_store=self._model_store, render_hook=self.render_hook)
|
||||
self._web_server = WebServer(kernel=self, model_store=self._model_store, template_renderer=self._template_renderer)
|
||||
self._external_storage_server = ExternalStorageServer(kernel=self, model_store=self._model_store)
|
||||
|
||||
logging.info("[{}] Starting application v{}...".format(self.get_name(), self.get_version()))
|
||||
self._plugin_store = PluginStore(kernel=self, model_store=self._model_store, template_renderer=self._template_renderer, web_server=self._web_server)
|
||||
@ -29,6 +31,7 @@ class Application:
|
||||
if variable:
|
||||
self._model_store.variable().update_by_name(variable.name, variable.as_int() + 1)
|
||||
|
||||
self._external_storage_server.run()
|
||||
self._web_server.run()
|
||||
|
||||
def signal_handler(self, signal, frame) -> None:
|
||||
@ -59,3 +62,7 @@ class Application:
|
||||
self._model_store.lang().set_lang(lang)
|
||||
self._model_store.variable().reload()
|
||||
self._plugin_store.reload_lang()
|
||||
|
||||
@property
|
||||
def external_storage_server(self):
|
||||
return self._external_storage_server
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.User import User
|
||||
@ -26,6 +26,8 @@ class AuthController(ObController):
|
||||
self._app.add_url_rule('/auth/user/delete/<user_id>', 'auth_user_delete', self.guard_auth(self._auth(self.auth_user_delete)), methods=['GET'])
|
||||
|
||||
def login(self):
|
||||
login_error = None
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('playlist'))
|
||||
|
||||
@ -39,12 +41,13 @@ class AuthController(ObController):
|
||||
login_user(user)
|
||||
return redirect(url_for('playlist'))
|
||||
else:
|
||||
flash(self.t('login_error_bad_credentials'), 'error')
|
||||
login_error = 'bad_credentials'
|
||||
else:
|
||||
flash(self.t('login_error_not_found'), 'error')
|
||||
login_error = 'not_found'
|
||||
|
||||
return render_template(
|
||||
'auth/login.jinja.html',
|
||||
login_error=login_error,
|
||||
last_username=request.form['username'] if 'username' in request.form else None
|
||||
)
|
||||
|
||||
@ -64,8 +67,8 @@ class AuthController(ObController):
|
||||
|
||||
return render_template(
|
||||
'auth/list.jinja.html',
|
||||
error=request.args.get('error', None),
|
||||
users=self._model_store.user().get_users(exclude=User.DEFAULT_USER if demo else None),
|
||||
plugin_core_api_enabled=self._model_store.variable().map().get('plugin_core_api_enabled').as_bool()
|
||||
)
|
||||
|
||||
def auth_user_add(self):
|
||||
@ -92,12 +95,10 @@ class AuthController(ObController):
|
||||
return redirect(url_for('auth_user_list'))
|
||||
|
||||
if user.id == str(current_user.id):
|
||||
flash(self.t('auth_user_delete_cant_delete_yourself'), 'error')
|
||||
return redirect(url_for('auth_user_list'))
|
||||
return redirect(url_for('auth_user_list', error='auth_user_delete_cant_delete_yourself'))
|
||||
|
||||
if self._model_store.user().count_all_enabled() == 1:
|
||||
flash(self.t('auth_user_delete_at_least_one_account'), 'error')
|
||||
return redirect(url_for('auth_user_list'))
|
||||
return redirect(url_for('auth_user_list', error='auth_user_delete_at_least_one_account'))
|
||||
|
||||
self._model_store.user().delete(user_id)
|
||||
return redirect(url_for('auth_user_list'))
|
||||
|
||||
@ -2,14 +2,14 @@ import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.Content import Content
|
||||
from src.model.enum.ContentType import ContentType
|
||||
from src.model.enum.ContentMetadata import ContentMetadata
|
||||
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
|
||||
from src.interface.ObController import ObController
|
||||
from src.service.ExternalStorageServer import ExternalStorageServer
|
||||
from src.util.utils import str_to_enum, get_optional_string
|
||||
from src.util.UtilFile import randomize_filename
|
||||
|
||||
@ -28,10 +28,11 @@ class ContentController(ObController):
|
||||
self._app.add_url_rule('/slideshow/content/move-folder', 'slideshow_content_folder_move', self._auth(self.slideshow_content_folder_move), methods=['POST'])
|
||||
self._app.add_url_rule('/slideshow/content/rename-folder', 'slideshow_content_folder_rename', self._auth(self.slideshow_content_folder_rename), methods=['POST'])
|
||||
self._app.add_url_rule('/slideshow/content/delete-folder', 'slideshow_content_folder_delete', self._auth(self.slideshow_content_folder_delete), methods=['GET'])
|
||||
self._app.add_url_rule('/slideshow/content/show/<content_id>', 'slideshow_content_show', self._auth(self.slideshow_content_show), methods=['GET'])
|
||||
self._app.add_url_rule('/slideshow/content/upload-bulk', 'slideshow_content_upload_bulk', self._auth(self.slideshow_content_upload_bulk), methods=['POST'])
|
||||
self._app.add_url_rule('/slideshow/content/delete-bulk-explr', 'slideshow_content_delete_bulk_explr', self._auth(self.slideshow_content_delete_bulk_explr), methods=['GET'])
|
||||
|
||||
def get_folder_context(self):
|
||||
def get_working_folder(self):
|
||||
working_folder_path = request.args.get('path', None)
|
||||
working_folder = None
|
||||
|
||||
@ -46,7 +47,7 @@ class ContentController(ObController):
|
||||
|
||||
def slideshow_content_list(self):
|
||||
self._model_store.variable().update_by_name('last_pillmenu_slideshow', 'slideshow_content_list')
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
slides_with_content = self._model_store.slide().get_all_indexed(attribute='content_id', multiple=True)
|
||||
|
||||
return render_template(
|
||||
@ -59,11 +60,11 @@ class ContentController(ObController):
|
||||
working_folder_children=self._model_store.folder().get_children(folder=working_folder, entity=FolderEntity.CONTENT, sort='created_at', ascending=False),
|
||||
enum_content_type=ContentType,
|
||||
enum_folder_entity=FolderEntity,
|
||||
external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint'),
|
||||
chroot_http_external_storage=self.get_external_storage_server().get_directory(),
|
||||
)
|
||||
|
||||
def slideshow_content_add(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
@ -85,7 +86,7 @@ class ContentController(ObController):
|
||||
return redirect(url_for('slideshow_content_list', **route_args))
|
||||
|
||||
def slideshow_content_upload_bulk(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
|
||||
for key in request.files:
|
||||
files = request.files.getlist(key)
|
||||
@ -110,30 +111,19 @@ class ContentController(ObController):
|
||||
if not content:
|
||||
return abort(404)
|
||||
|
||||
vargs = {}
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
edit_view = 'slideshow/contents/edit.jinja.html'
|
||||
|
||||
if content.type == ContentType.COMPOSITION:
|
||||
edit_view = 'slideshow/contents/edit-composition.jinja.html'
|
||||
vargs['folders_tree'] = self._model_store.folder().get_folder_tree(FolderEntity.CONTENT)
|
||||
vargs['foldered_contents'] = self._model_store.content().get_all_indexed('folder_id', multiple=True)
|
||||
elif content.type == ContentType.TEXT:
|
||||
edit_view = 'slideshow/contents/edit-text.jinja.html'
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
|
||||
return render_template(
|
||||
edit_view,
|
||||
'slideshow/contents/edit.jinja.html',
|
||||
content=content,
|
||||
working_folder_path=working_folder_path,
|
||||
working_folder=working_folder,
|
||||
enum_content_type=ContentType,
|
||||
enum_content_metadata=ContentMetadata,
|
||||
external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint'),
|
||||
**vargs
|
||||
chroot_http_external_storage=self.get_external_storage_server().get_directory(),
|
||||
)
|
||||
|
||||
def slideshow_content_save(self, content_id: int = 0):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
@ -146,24 +136,22 @@ class ContentController(ObController):
|
||||
)
|
||||
self._post_update()
|
||||
|
||||
flash(self.t('common_saved'), 'success')
|
||||
|
||||
return redirect(url_for('slideshow_content_edit', content_id=content_id))
|
||||
return redirect(url_for('slideshow_content_edit', content_id=content_id, saved=1))
|
||||
|
||||
def slideshow_content_delete(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
error = self.delete_content_by_id(request.args.get('id'))
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
error_tuple = self.delete_content_by_id(request.args.get('id'))
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('slideshow_content_list', **route_args))
|
||||
|
||||
def slideshow_content_rename(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
self._model_store.content().update_form(
|
||||
id=request.form['id'],
|
||||
name=request.form['name'],
|
||||
@ -194,7 +182,7 @@ class ContentController(ObController):
|
||||
return redirect(url_for('slideshow_content_list', path=path))
|
||||
|
||||
def slideshow_content_folder_add(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
|
||||
self._model_store.folder().add_folder(
|
||||
entity=FolderEntity.CONTENT,
|
||||
@ -205,7 +193,7 @@ class ContentController(ObController):
|
||||
return redirect(url_for('slideshow_content_list', path=working_folder_path))
|
||||
|
||||
def slideshow_content_folder_rename(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
self._model_store.folder().rename_folder(
|
||||
folder_id=request.form['id'],
|
||||
name=request.form['name'],
|
||||
@ -214,7 +202,7 @@ class ContentController(ObController):
|
||||
return redirect(url_for('slideshow_content_list', path=working_folder_path))
|
||||
|
||||
def slideshow_content_folder_move(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
entity_ids = request.form['entity_ids'].split(',')
|
||||
folder_ids = request.form['folder_ids'].split(',')
|
||||
|
||||
@ -237,36 +225,44 @@ class ContentController(ObController):
|
||||
return redirect(url_for('slideshow_content_list', path=working_folder_path))
|
||||
|
||||
def slideshow_content_folder_delete(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
error = self.delete_folder_by_id(request.args.get('id'))
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
error_tuple = self.delete_folder_by_id(request.args.get('id'))
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
|
||||
if error:
|
||||
flash(self.t(error), 'error')
|
||||
if error_tuple:
|
||||
route_args[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('slideshow_content_list', **route_args))
|
||||
|
||||
def slideshow_content_show(self, content_id: int = 0):
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
return abort(404)
|
||||
|
||||
return redirect(self._model_store.content().resolve_content_location(content))
|
||||
|
||||
def slideshow_content_delete_bulk_explr(self):
|
||||
working_folder_path, working_folder = self.get_folder_context()
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
entity_ids = request.args.get('entity_ids', '').split(',')
|
||||
folder_ids = request.args.get('folder_ids', '').split(',')
|
||||
route_args_dict = {"path": working_folder_path}
|
||||
|
||||
for id in entity_ids:
|
||||
if id:
|
||||
error = self.delete_content_by_id(id)
|
||||
error_tuple = self.delete_content_by_id(id)
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
for id in folder_ids:
|
||||
if id:
|
||||
error = self.delete_folder_by_id(id)
|
||||
error_tuple = self.delete_folder_by_id(id)
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('slideshow_content_list', **route_args_dict))
|
||||
|
||||
@ -277,7 +273,7 @@ class ContentController(ObController):
|
||||
return None
|
||||
|
||||
if self._model_store.slide().count_slides_for_content(content.id) > 0:
|
||||
return 'slideshow_content_referenced_in_slide_error'.replace('%contentName%', content.name)
|
||||
return 'referenced_in_slide_error', content.name
|
||||
|
||||
self._model_store.content().delete(content.id)
|
||||
self._post_update()
|
||||
@ -293,7 +289,7 @@ class ContentController(ObController):
|
||||
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
|
||||
|
||||
if content_counter > 0 or folder_counter:
|
||||
return self.t('common_folder_not_empty_error').replace('%folderName%', folder.name)
|
||||
return 'folder_not_empty_error', folder.name
|
||||
|
||||
self._model_store.folder().delete(id=folder.id)
|
||||
self._post_update()
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
from typing import Optional
|
||||
from flask import Flask, send_file, render_template_string, jsonify, request
|
||||
|
||||
from flask import Flask, send_file, render_template_string, jsonify
|
||||
|
||||
from src.interface.ObController import ObController
|
||||
# from src.util.UtilChromecast import fetch_friendly_names, cast_url
|
||||
|
||||
|
||||
class CoreController(ObController):
|
||||
@ -10,8 +9,6 @@ class CoreController(ObController):
|
||||
def register(self):
|
||||
self._app.add_url_rule('/manifest.json', 'manifest', self.manifest, methods=['GET'])
|
||||
self._app.add_url_rule('/favicon.ico', 'favicon', self.favicon, methods=['GET'])
|
||||
# self._app.add_url_rule('/cast-scan', 'cast_scan', self.cast_scan, methods=['GET'])
|
||||
# self._app.add_url_rule('/cast-url', 'cast_url', self.cast_url, methods=['POST'])
|
||||
|
||||
def manifest(self):
|
||||
with open("{}/manifest.jinja.json".format(self.get_template_dir()), 'r') as file:
|
||||
@ -22,17 +19,4 @@ class CoreController(ObController):
|
||||
return self._app.response_class(rendered_content, mimetype='application/json')
|
||||
|
||||
def favicon(self):
|
||||
return send_file("{}/favicon.ico".format(self.get_web_dir()), mimetype='image/x-icon')
|
||||
|
||||
# def cast_scan(self):
|
||||
# return jsonify({
|
||||
# 'devices': fetch_friendly_names(discovery_timeout=5)
|
||||
# })
|
||||
#
|
||||
# def cast_url(self):
|
||||
# data = request.get_json()
|
||||
# success = cast_url(friendly_name=data.get('device'), url=data.get('url'), discovery_timeout=5)
|
||||
#
|
||||
# return jsonify({
|
||||
# 'success': success
|
||||
# })
|
||||
return send_file("{}/favicon.ico".format(self.get_web_dir()), mimetype='image/x-icon')
|
||||
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.NodePlayer import NodePlayer
|
||||
from src.interface.ObController import ObController
|
||||
@ -108,19 +108,18 @@ class FleetNodePlayerController(ObController):
|
||||
)
|
||||
self._post_update()
|
||||
|
||||
flash(self.t('common_saved'), 'success')
|
||||
|
||||
# return redirect(url_for('fleet_node_player_edit', node_player_id=node_player_id, saved=1))
|
||||
return redirect(url_for('fleet_node_player_list', path=working_folder_path))
|
||||
|
||||
def fleet_node_player_delete(self):
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
error = self.delete_node_player_by_id(request.args.get('id'))
|
||||
error_tuple = self.delete_node_player_by_id(request.args.get('id'))
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('fleet_node_player_list', **route_args))
|
||||
|
||||
@ -201,13 +200,13 @@ class FleetNodePlayerController(ObController):
|
||||
|
||||
def fleet_node_player_folder_delete(self):
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
error = self.delete_folder_by_id(request.args.get('id'))
|
||||
error_tuple = self.delete_folder_by_id(request.args.get('id'))
|
||||
route_args = {
|
||||
"path": working_folder_path,
|
||||
}
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('fleet_node_player_list', **route_args))
|
||||
|
||||
@ -219,17 +218,17 @@ class FleetNodePlayerController(ObController):
|
||||
|
||||
for id in entity_ids:
|
||||
if id:
|
||||
error = self.delete_node_player_by_id(id)
|
||||
error_tuple = self.delete_node_player_by_id(id)
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
for id in folder_ids:
|
||||
if id:
|
||||
error = self.delete_folder_by_id(id)
|
||||
error_tuple = self.delete_folder_by_id(id)
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
if error_tuple:
|
||||
route_args_dict[error_tuple[0]] = error_tuple[1]
|
||||
|
||||
return redirect(url_for('fleet_node_player_list', **route_args_dict))
|
||||
|
||||
@ -253,7 +252,7 @@ class FleetNodePlayerController(ObController):
|
||||
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
|
||||
|
||||
if node_player_counter > 0 or folder_counter:
|
||||
return self.t('common_folder_not_empty_error').replace('%folderName%', folder.name)
|
||||
return 'folder_not_empty_error', folder.name
|
||||
|
||||
self._model_store.folder().delete(id=folder.id)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.NodePlayerGroup import NodePlayerGroup
|
||||
from src.model.enum.FolderEntity import FolderEntity
|
||||
@ -43,6 +43,7 @@ class FleetNodePlayerGroupController(ObController):
|
||||
|
||||
return render_template(
|
||||
'fleet/player-group/list.jinja.html',
|
||||
error=request.args.get('error', None),
|
||||
current_player_group=current_player_group,
|
||||
node_player_groups=node_player_groups,
|
||||
pcounters=pcounters,
|
||||
@ -85,8 +86,7 @@ class FleetNodePlayerGroupController(ObController):
|
||||
|
||||
def fleet_node_player_group_delete(self, player_group_id: int):
|
||||
if self._model_store.node_player().count_node_players_for_group(player_group_id) > 0:
|
||||
flash(self.t('node_player_group_delete_has_node_player'), 'error')
|
||||
return redirect(url_for('fleet_node_player_group_list', player_group_id=player_group_id))
|
||||
return redirect(url_for('fleet_node_player_group_list', player_group_id=player_group_id, error='node_player_group_delete_has_node_player'))
|
||||
|
||||
self._model_store.node_player_group().delete(player_group_id)
|
||||
return redirect(url_for('fleet_node_player_group'))
|
||||
|
||||
@ -1,20 +1,18 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort, send_file, Response
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort
|
||||
from pathlib import Path
|
||||
|
||||
from src.model.entity.Slide import Slide
|
||||
from src.model.entity.Content import Content
|
||||
from src.model.enum.ContentType import ContentType
|
||||
from src.exceptions.NoFallbackPlaylistException import NoFallbackPlaylistException
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import get_safe_cron_descriptor, is_cron_in_datetime_moment, is_cron_in_week_moment, is_now_after_cron_date_time_moment, is_now_after_cron_week_moment, decode_uri_component
|
||||
from src.util.utils import get_safe_cron_descriptor, is_cron_in_datetime_moment, is_cron_in_week_moment, is_now_after_cron_date_time_moment, is_now_after_cron_week_moment
|
||||
from src.util.UtilNetwork import get_safe_remote_addr, get_network_interfaces
|
||||
from src.model.enum.AnimationSpeed import animation_speed_duration
|
||||
|
||||
@ -27,32 +25,20 @@ class PlayerController(ObController):
|
||||
self._app.add_url_rule('/player/default', 'player_default', self.player_default, 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('/serve/content/<content_type>/<content_id>/<content_location>', 'serve_content_file', self.serve_content_file, methods=['GET'])
|
||||
self._app.add_url_rule('/serve/content/composition/<content_id>', 'serve_content_composition', self.serve_content_composition, methods=['GET'])
|
||||
|
||||
def player(self, playlist_slug_or_id: str = ''):
|
||||
preview_playlist = request.args.get('preview_playlist')
|
||||
preview_content_id = request.args.get('preview_content_id')
|
||||
playlist_id = None
|
||||
playlist_slug_or_id = self._get_dynamic_playlist_id(playlist_slug_or_id)
|
||||
|
||||
if not preview_content_id:
|
||||
query = " (slug = ? OR id = ?) "
|
||||
query_args = {
|
||||
"slug": playlist_slug_or_id,
|
||||
"id": playlist_slug_or_id,
|
||||
}
|
||||
current_playlist = self._model_store.playlist().get_one_by("slug = ? OR id = ?", {
|
||||
"slug": playlist_slug_or_id,
|
||||
"id": playlist_slug_or_id
|
||||
})
|
||||
|
||||
if not preview_playlist:
|
||||
query = query + " AND enabled = ? "
|
||||
query_args["enabled"] = True
|
||||
if playlist_slug_or_id and not current_playlist:
|
||||
return abort(404)
|
||||
|
||||
current_playlist = self._model_store.playlist().get_one_by(query, query_args)
|
||||
|
||||
if playlist_slug_or_id and not current_playlist:
|
||||
return abort(404)
|
||||
|
||||
playlist_id = current_playlist.id if current_playlist else None
|
||||
playlist_id = current_playlist.id if current_playlist else None
|
||||
|
||||
try:
|
||||
items = self._get_playlist(playlist_id=playlist_id, preview_content_id=preview_content_id)
|
||||
@ -66,8 +52,6 @@ class PlayerController(ObController):
|
||||
slide_animation_entrance_effect = request.args.get('animation_effect', self._model_store.variable().get_one_by_name('slide_animation_entrance_effect').eval())
|
||||
slide_animation_exit_effect = request.args.get('slide_animation_exit_effect', self._model_store.variable().get_one_by_name('slide_animation_exit_effect').eval())
|
||||
|
||||
|
||||
|
||||
return render_template(
|
||||
'player/player.jinja.html',
|
||||
items=items,
|
||||
@ -86,8 +70,7 @@ class PlayerController(ObController):
|
||||
interfaces=[iface['ip_address'] for iface in get_network_interfaces()],
|
||||
external_url=self._model_store.variable().get_one_by_name('external_url').as_string().strip(),
|
||||
time_with_seconds=self._model_store.variable().get_one_by_name('default_slide_time_with_seconds'),
|
||||
noplaylist=request.args.get('noplaylist', '0') == '1',
|
||||
hard_refresh_request=self._model_store.variable().get_one_by_name("refresh_player_request").as_int()
|
||||
noplaylist=request.args.get('noplaylist', '0') == '1'
|
||||
)
|
||||
|
||||
def player_playlist(self, playlist_slug_or_id: str = ''):
|
||||
@ -127,7 +110,7 @@ class PlayerController(ObController):
|
||||
preview_content = self._model_store.content().get(preview_content_id) if preview_content_id else None
|
||||
preview_mode = preview_content is not None
|
||||
|
||||
if not preview_mode and (playlist_id == 0 or not playlist_id):
|
||||
if playlist_id == 0 or not playlist_id:
|
||||
playlist = self._model_store.playlist().get_one_by(query="fallback = 1")
|
||||
|
||||
if playlist:
|
||||
@ -137,9 +120,8 @@ class PlayerController(ObController):
|
||||
|
||||
enabled_slides = [Slide(content_id=preview_content.id, duration=1000000)] if preview_mode else self._model_store.slide().get_slides(enabled=True, playlist_id=playlist_id)
|
||||
slides = self._model_store.slide().to_dict(enabled_slides)
|
||||
content_ids = [str(slide['content_id']) for slide in slides if slide['content_id'] is not None]
|
||||
contents = self._model_store.content().get_all_indexed(query="id IN ({})".format(','.join(content_ids)))
|
||||
playlist = self._model_store.playlist().get(playlist_id) if not preview_mode else None
|
||||
contents = self._model_store.content().get_all_indexed()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
position = 9999
|
||||
|
||||
playlist_loop = []
|
||||
@ -154,27 +136,24 @@ class PlayerController(ObController):
|
||||
|
||||
content = contents[int(slide['content_id'])]
|
||||
slide['name'] = content.name
|
||||
slide['location'] = content.location
|
||||
slide['type'] = content.type.value
|
||||
slide['location'] = self._model_store.content().resolve_content_location(content)
|
||||
|
||||
if slide['type'] == ContentType.EXTERNAL_STORAGE.value:
|
||||
mount_point_dir = Path(self._model_store.config().map().get('external_storage_mountpoint'), content.location)
|
||||
mount_point_dir = Path(self.get_external_storage_server().get_directory(), slide['location'])
|
||||
if mount_point_dir.is_dir():
|
||||
for file in mount_point_dir.iterdir():
|
||||
if file.is_file() and not file.stem.startswith('.'):
|
||||
virtual_content = Content(
|
||||
id=content.id,
|
||||
name=file.stem,
|
||||
location=str(Path(mount_point_dir, file.name)),
|
||||
type=ContentType.guess_content_type_file(str(file.resolve())),
|
||||
)
|
||||
slide = dict(slide)
|
||||
slide['id'] = hashlib.md5(str(file).encode('utf-8')).hexdigest()
|
||||
slide['position'] = position
|
||||
slide['delegate_duration'] = 1 if virtual_content.type == ContentType.VIDEO else 0
|
||||
slide['name'] = file.name
|
||||
slide['type'] = virtual_content.type.value
|
||||
slide['location'] = self._model_store.content().resolve_content_location(virtual_content)
|
||||
slide['type'] = ContentType.guess_content_type_file(str(file.resolve())).value
|
||||
slide['name'] = file.stem
|
||||
slide['delegate_duration'] = 1 if slide['type'] == ContentType.VIDEO.value else 0
|
||||
slide['location'] = "{}/{}".format(
|
||||
self._model_store.content().resolve_content_location(content),
|
||||
file.name
|
||||
)
|
||||
self._check_slide_enablement(playlist_loop, playlist_notifications, slide)
|
||||
position = position + 1
|
||||
else:
|
||||
@ -218,50 +197,3 @@ class PlayerController(ObController):
|
||||
return
|
||||
|
||||
loop.append(slide)
|
||||
|
||||
def serve_content_file(self, content_location, content_type, content_id):
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
abort(404, 'Content not found')
|
||||
|
||||
content_location = decode_uri_component(content_location)
|
||||
|
||||
content_path = str(Path(self.get_application_dir(), content_location))
|
||||
|
||||
if content_type == ContentType.EXTERNAL_STORAGE.value:
|
||||
content_path = str(Path(self._model_store.config().map().get('external_storage_mountpoint'), content_location))
|
||||
|
||||
if not os.path.exists(content_path) or '..' in content_path:
|
||||
abort(404, 'Content not found')
|
||||
|
||||
if not self._model_store.variable().get_one_by_name('player_content_cache').as_bool():
|
||||
response = send_file(content_path)
|
||||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
return response
|
||||
|
||||
content_path_hash = hashlib.sha256(str(content_path).encode()).hexdigest()
|
||||
etag = f'"{content_path_hash}-{content_id}-{os.path.getmtime(content_path)}"'
|
||||
|
||||
if_none_match = request.headers.get('If-None-Match')
|
||||
if if_none_match == etag:
|
||||
return Response(status=304)
|
||||
|
||||
response = send_file(content_path)
|
||||
response.headers['Cache-Control'] = 'public, max-age=3153600000' # 100 years
|
||||
response.headers['ETag'] = etag
|
||||
|
||||
return response
|
||||
|
||||
def serve_content_composition(self, content_id):
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content or content.type != ContentType.COMPOSITION:
|
||||
abort(404, 'Content not found')
|
||||
|
||||
return render_template(
|
||||
'player/content/composition.jinja.html',
|
||||
content=content,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.model.enum.FolderEntity import FolderEntity
|
||||
@ -35,6 +35,7 @@ class PlaylistController(ObController):
|
||||
|
||||
return render_template(
|
||||
'playlist/list.jinja.html',
|
||||
error=request.args.get('error', None),
|
||||
current_playlist=current_playlist,
|
||||
playlists=playlists,
|
||||
durations=durations,
|
||||
@ -54,8 +55,7 @@ class PlaylistController(ObController):
|
||||
playlist = Playlist(
|
||||
name=request.form['name'],
|
||||
enabled=True,
|
||||
time_sync=False,
|
||||
fallback=self._model_store.playlist().count_fallbacks() == 0
|
||||
time_sync=False
|
||||
)
|
||||
|
||||
try:
|
||||
@ -70,8 +70,7 @@ class PlaylistController(ObController):
|
||||
id=request.form['id'],
|
||||
name=request.form['name'],
|
||||
time_sync=True if 'time_sync' in request.form else False,
|
||||
enabled=True if 'enabled' in request.form else False,
|
||||
fallback=True if self._model_store.playlist().count_fallbacks() == 0 else None
|
||||
enabled=True if 'enabled' in request.form else False
|
||||
)
|
||||
return redirect(url_for('playlist_list', playlist_id=request.form['id']))
|
||||
|
||||
@ -82,12 +81,10 @@ class PlaylistController(ObController):
|
||||
abort(404)
|
||||
|
||||
if self._model_store.slide().count_slides_for_playlist(playlist_id) > 0:
|
||||
flash(self.t('playlist_delete_has_slides'), 'error')
|
||||
return redirect(url_for('playlist_list', playlist_id=playlist_id))
|
||||
return redirect(url_for('playlist_list', playlist_id=playlist_id, error='playlist_delete_has_slides'))
|
||||
|
||||
if self._model_store.node_player_group().count_node_player_groups_for_playlist(playlist_id) > 0:
|
||||
flash(self.t('playlist_delete_has_node_player_groups'), 'error')
|
||||
return redirect(url_for('playlist_list', playlist_id=playlist_id))
|
||||
return redirect(url_for('playlist_list', playlist_id=playlist_id, error='playlist_delete_has_node_player_groups'))
|
||||
|
||||
self._model_store.playlist().delete(playlist_id)
|
||||
return redirect(url_for('playlist'))
|
||||
|
||||
@ -2,7 +2,7 @@ import time
|
||||
import json
|
||||
import threading
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for
|
||||
from typing import Optional
|
||||
|
||||
from src.service.ModelStore import ModelStore
|
||||
@ -40,8 +40,7 @@ class SettingsController(ObController):
|
||||
error = self._pre_update(request.form['id'])
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
return redirect(url_for('settings_variable_list'))
|
||||
return redirect(url_for('settings_variable_list', error=error))
|
||||
|
||||
self._model_store.variable().update_form(request.form['id'], request.form['value'])
|
||||
redirect_response = self._post_update(request.form['id'])
|
||||
@ -55,8 +54,7 @@ class SettingsController(ObController):
|
||||
error = self._pre_update(request.form['id'])
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
return redirect(url_for('settings_variable_plugin_list'))
|
||||
return redirect(url_for('settings_variable_plugin_list', error=error))
|
||||
|
||||
self._model_store.variable().update_form(request.form['id'], request.form['value'])
|
||||
redirect_response = self._post_update(request.form['id'])
|
||||
@ -81,8 +79,7 @@ class SettingsController(ObController):
|
||||
|
||||
if variable.name == 'slide_upload_limit':
|
||||
self.reload_web_server()
|
||||
flash(self.t('common_restart_needed'), 'warning')
|
||||
return redirect(url_for('settings_variable_list'))
|
||||
return redirect(url_for('settings_variable_list', warning='common_restart_needed'))
|
||||
|
||||
if variable.name == 'fleet_player_enabled':
|
||||
self.reload_web_server()
|
||||
@ -101,10 +98,7 @@ class SettingsController(ObController):
|
||||
thread = threading.Thread(target=self.plugin_update)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
flash(self.t('common_restart_needed'), 'warning')
|
||||
return redirect(url_for('settings_variable_plugin_list'))
|
||||
|
||||
flash(self.t('common_saved'), 'success')
|
||||
return redirect(url_for('settings_variable_plugin_list', warning='common_restart_needed'))
|
||||
|
||||
def plugin_update(self) -> None:
|
||||
restart()
|
||||
|
||||
@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort, flash
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.Slide import Slide
|
||||
@ -102,11 +102,11 @@ class SlideController(ObController):
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
def slideshow_player_refresh(self):
|
||||
referrer_path = self.get_referrer_path()
|
||||
max_timeout_value = self._model_store.variable().get_one_by_name('polling_interval').as_string()
|
||||
flash(self.t('slideshow_slide_refresh_player_success').replace('%time%', max_timeout_value), 'success:refresh')
|
||||
self._model_store.variable().update_by_name("refresh_player_request", time.time())
|
||||
return redirect(referrer_path)
|
||||
max_timeout_value = self._model_store.variable().get_one_by_name('polling_interval').as_int()
|
||||
query_params = '{}={}'.format('refresh_player', max_timeout_value)
|
||||
next_url = request.args.get('next')
|
||||
return redirect('{}{}{}'.format(next_url, '&' if '?' in next_url else '?', query_params))
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
||||
|
||||
@ -69,6 +69,5 @@ class SysinfoController(ObController):
|
||||
def sysinfo_get_ipaddr(self):
|
||||
return jsonify({
|
||||
'external_url': self._model_store.variable().get_one_by_name('external_url').as_string().strip(),
|
||||
'interfaces': [iface['ip_address'] for iface in get_network_interfaces()],
|
||||
'hard_refresh_request': self._model_store.variable().get_one_by_name("refresh_player_request").as_int()
|
||||
'interfaces': [iface['ip_address'] for iface in get_network_interfaces()]
|
||||
})
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
|
||||
class HttpClientException(HTTPException):
|
||||
pass
|
||||
@ -1,7 +1,6 @@
|
||||
import abc
|
||||
|
||||
from typing import Optional, List, Dict, Union
|
||||
from flask import request
|
||||
from src.service.TemplateRenderer import TemplateRenderer
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.interface.ObPlugin import ObPlugin
|
||||
@ -41,30 +40,11 @@ class ObController(abc.ABC):
|
||||
def reload_lang(self, lang: str):
|
||||
self._kernel.reload_lang(lang)
|
||||
|
||||
def get_application_dir(self):
|
||||
return self._kernel.get_application_dir()
|
||||
|
||||
def t(self, token) -> Union[Dict, str]:
|
||||
return self._model_store.lang().translate(token)
|
||||
|
||||
def get_external_storage_server(self):
|
||||
return self._kernel.external_storage_server
|
||||
|
||||
def render_view(self, template_file: str, **parameters: dict) -> str:
|
||||
return self._template_renderer.render_view(template_file, self.plugin(), **parameters)
|
||||
|
||||
def api(self):
|
||||
return self._web_server.api
|
||||
|
||||
def get_referrer_path(self):
|
||||
referer_url = request.referrer
|
||||
if referer_url:
|
||||
return '/' + referer_url.replace(request.host_url, '').split('?')[0]
|
||||
return None
|
||||
|
||||
def get_referrer_rule(self):
|
||||
referer_path = self.get_referrer_path()
|
||||
|
||||
if referer_path:
|
||||
for rule in self._app.url_map.iter_rules():
|
||||
if referer_path == rule.rule.split('/<')[0]:
|
||||
return rule.rule
|
||||
|
||||
return None
|
||||
|
||||
@ -38,10 +38,6 @@ class ObPlugin(abc.ABC):
|
||||
def use_description(self) -> str:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def use_help_on_activation(self) -> Optional[str]:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def use_variables(self) -> List[Variable]:
|
||||
pass
|
||||
@ -50,10 +46,6 @@ class ObPlugin(abc.ABC):
|
||||
def use_hooks_registrations(self) -> List[HookRegistration]:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_version(self) -> str:
|
||||
pass
|
||||
|
||||
def get_directory(self) -> Optional[str]:
|
||||
return self._plugin_dir
|
||||
|
||||
@ -90,10 +82,7 @@ class ObPlugin(abc.ABC):
|
||||
def add_functional_hook_registration(self, hook: HookType, priority: int = 0, function=None) -> FunctionalHookRegistration:
|
||||
return FunctionalHookRegistration(plugin=self, hook=hook, priority=priority, function=function)
|
||||
|
||||
def translate(self, token, resolve=True) -> Union[Dict, str]:
|
||||
if not token:
|
||||
token = '<UNKNOWN>'
|
||||
|
||||
def translate(self, token, resolve=False) -> Union[Dict, str]:
|
||||
token = token if token.startswith(self.use_id()) else "{}_{}".format(self.use_id(), token)
|
||||
return self._model_store.lang().translate(token) if resolve else token
|
||||
|
||||
|
||||
@ -9,17 +9,18 @@ load_dotenv()
|
||||
|
||||
class ConfigManager:
|
||||
|
||||
APPLICATION_NAME = "Obscreen"
|
||||
DEFAULT_PORT = 5000
|
||||
DEFAULT_PORT_HTTP_EXTERNAL_STORAGE = 5001
|
||||
VERSION_FILE = 'version.txt'
|
||||
|
||||
def __init__(self, replacers: Dict):
|
||||
self._replacers = replacers
|
||||
self._CONFIG = {
|
||||
'application_name': self.APPLICATION_NAME,
|
||||
'version': None,
|
||||
'demo': False,
|
||||
'external_storage_mountpoint': '%application_dir%/var/run/storage',
|
||||
'port_http_external_storage': self.DEFAULT_PORT_HTTP_EXTERNAL_STORAGE,
|
||||
'bind_http_external_storage': '0.0.0.0',
|
||||
'chroot_http_external_storage': '%application_dir%/var/run/storage',
|
||||
'port': self.DEFAULT_PORT,
|
||||
'bind': '0.0.0.0',
|
||||
'debug': False,
|
||||
@ -53,7 +54,9 @@ class ConfigManager:
|
||||
parser.add_argument('--log-level', '-ll', default=self._CONFIG['log_level'], help='Log Level')
|
||||
parser.add_argument('--log-stdout', '-ls', default=self._CONFIG['log_stdout'], action='store_true', help='Log to standard output')
|
||||
parser.add_argument('--demo', '-o', default=self._CONFIG['demo'], help='Demo mode to showcase obscreen in a sandbox')
|
||||
parser.add_argument('--external-storage-mountpoint', '-e', default=self._CONFIG['external_storage_mountpoint'], help='Mountpoint directory of external storage')
|
||||
parser.add_argument('--port-http-external-storage', '-bx', default=self._CONFIG['port_http_external_storage'], help='Port of http server serving external storage')
|
||||
parser.add_argument('--bind-http-external-storage', '-px', default=self._CONFIG['bind_http_external_storage'], help='Bind address of http server serving external storage')
|
||||
parser.add_argument('--chroot-http-external-storage', '-cx', default=self._CONFIG['chroot_http_external_storage'], help='Chroot directory of http server serving external storage')
|
||||
parser.add_argument('--version', '-v', default=None, action='store_true', help='Get version number')
|
||||
|
||||
return parser.parse_args()
|
||||
@ -69,8 +72,12 @@ class ConfigManager:
|
||||
self._CONFIG['debug'] = args.debug
|
||||
if args.demo:
|
||||
self._CONFIG['demo'] = args.demo
|
||||
if args.external_storage_mountpoint:
|
||||
self._CONFIG['external_storage_mountpoint'] = args.external_storage_mountpoint
|
||||
if args.port_http_external_storage:
|
||||
self._CONFIG['port_http_external_storage'] = args.port_http_external_storage
|
||||
if args.bind_http_external_storage:
|
||||
self._CONFIG['bind_http_external_storage'] = args.bind_http_external_storage
|
||||
if args.chroot_http_external_storage:
|
||||
self._CONFIG['chroot_http_external_storage'] = args.chroot_http_external_storage
|
||||
if args.log_file:
|
||||
self._CONFIG['log_file'] = args.log_file
|
||||
if args.secret_key:
|
||||
@ -80,7 +87,7 @@ class ConfigManager:
|
||||
if args.log_stdout:
|
||||
self._CONFIG['log_stdout'] = args.log_stdout
|
||||
if args.version:
|
||||
print("{} version v{} (https://github.com/jr-k/obscreen)".format(self.APPLICATION_NAME, self._CONFIG['version']))
|
||||
print("Obscreen version v{} (https://github.com/jr-k/obscreen)".format(self._CONFIG['version']))
|
||||
sys.exit(0)
|
||||
|
||||
def load_from_env(self) -> None:
|
||||
|
||||
@ -2,11 +2,9 @@ import os
|
||||
|
||||
from typing import Dict, Optional, List, Tuple, Union
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from flask import url_for
|
||||
|
||||
from src.model.entity.Content import Content
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.model.enum.ContentMetadata import ContentMetadata
|
||||
from src.model.enum.ContentType import ContentType
|
||||
from src.util.utils import get_yt_video_id
|
||||
from src.manager.DatabaseManager import DatabaseManager
|
||||
@ -17,9 +15,7 @@ from src.manager.VariableManager import VariableManager
|
||||
from src.service.ModelManager import ModelManager
|
||||
from src.util.UtilFile import randomize_filename
|
||||
from src.util.UtilNetwork import get_preferred_ip_address
|
||||
from src.util.UtilVideo import get_video_metadata
|
||||
from src.util.UtilPicture import get_picture_metadata
|
||||
from src.util.utils import encode_uri_component
|
||||
from src.util.UtilVideo import mp4_duration_with_ffprobe
|
||||
|
||||
|
||||
class ContentManager(ModelManager):
|
||||
@ -30,8 +26,7 @@ class ContentManager(ModelManager):
|
||||
"name CHAR(255)",
|
||||
"type CHAR(30)",
|
||||
"location TEXT",
|
||||
"duration FLOAT",
|
||||
"metadata TEXT",
|
||||
"duration INTEGER",
|
||||
"folder_id INTEGER",
|
||||
"created_by CHAR(255)",
|
||||
"updated_by CHAR(255)",
|
||||
@ -43,12 +38,6 @@ class ContentManager(ModelManager):
|
||||
super().__init__(lang_manager, database_manager, user_manager, variable_manager)
|
||||
self._config_manager = config_manager
|
||||
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
|
||||
self.pre_migrate()
|
||||
|
||||
def pre_migrate(self):
|
||||
if not self._variable_manager.get_one_by_name('refresh_all_metadata').as_bool():
|
||||
self.refresh_all_metadata()
|
||||
self._variable_manager.update_by_name('refresh_all_metadata', True)
|
||||
|
||||
def hydrate_object(self, raw_content: dict, id: int = None) -> Content:
|
||||
if id:
|
||||
@ -82,12 +71,10 @@ class ContentManager(ModelManager):
|
||||
def get_all(self, sort: Optional[str] = 'created_at', ascending=False) -> List[Content]:
|
||||
return self.hydrate_list(self._db.get_all(table_name=self.TABLE_NAME, sort=sort, ascending=ascending))
|
||||
|
||||
def get_all_indexed(self, attribute: str = 'id', multiple=False, query: str = None) -> Dict[str, Content]:
|
||||
def get_all_indexed(self, attribute: str = 'id', multiple=False) -> Dict[str, Content]:
|
||||
index = {}
|
||||
|
||||
items = self.get_by(query) if query else self.get_contents()
|
||||
|
||||
for item in items:
|
||||
for item in self.get_contents():
|
||||
id = getattr(item, attribute)
|
||||
if multiple:
|
||||
if id not in index:
|
||||
@ -105,17 +92,14 @@ class ContentManager(ModelManager):
|
||||
for content_id, edits in edits_contents.items():
|
||||
self._db.update_by_id(self.TABLE_NAME, content_id, edits)
|
||||
|
||||
def get_contents(self, slide_id: Optional[int] = None, folder_id: Optional[int] = None) -> List[Content]:
|
||||
def get_contents(self, slide_id: Optional[id] = None, folder_id: Optional[id] = None) -> List[Content]:
|
||||
query = " 1=1 "
|
||||
|
||||
if slide_id:
|
||||
query = "{} {}".format(query, "AND slide_id = {}".format(slide_id))
|
||||
|
||||
if folder_id is not None:
|
||||
if folder_id == 0:
|
||||
query = "{} {}".format(query, "AND folder_id is null")
|
||||
else:
|
||||
query = "{} {}".format(query, "AND folder_id = {}".format(folder_id))
|
||||
if folder_id:
|
||||
query = "{} {}".format(query, "AND folder_id = {}".format(folder_id))
|
||||
|
||||
return self.get_by(query=query)
|
||||
|
||||
@ -143,15 +127,14 @@ class ContentManager(ModelManager):
|
||||
def post_delete(self, content_id: str) -> str:
|
||||
return content_id
|
||||
|
||||
def update_form(self, id: int, name: Optional[str] = None, location: Optional[str] = None, metadata: Optional[str] = None) -> Optional[Content]:
|
||||
def update_form(self, id: int, name: str, location: Optional[str] = None) -> Optional[Content]:
|
||||
content = self.get(id)
|
||||
|
||||
if not content:
|
||||
return
|
||||
|
||||
form = {
|
||||
"name": name if isinstance(name, str) else content.name,
|
||||
"metadata": metadata if isinstance(metadata, str) else content.metadata
|
||||
"name": name,
|
||||
}
|
||||
|
||||
if location is not None and location:
|
||||
@ -210,29 +193,16 @@ class ContentManager(ModelManager):
|
||||
object_path = os.path.join(upload_dir, object_name)
|
||||
object.save(object_path)
|
||||
content.location = object_path
|
||||
self.set_metadata(content)
|
||||
|
||||
if type == ContentType.VIDEO:
|
||||
content.duration = mp4_duration_with_ffprobe(content.location)
|
||||
|
||||
else:
|
||||
content.location = ContentType.get_initial_location(content.type, location)
|
||||
content.location = location
|
||||
|
||||
self.add_form(content)
|
||||
return self.get_one_by(query="uuid = '{}'".format(content.uuid))
|
||||
|
||||
def set_metadata(self, content: Content) -> str:
|
||||
if content.type == ContentType.VIDEO:
|
||||
width, height, duration = get_video_metadata(content.location)
|
||||
content.duration = duration
|
||||
content.set_metadata(ContentMetadata.DURATION, duration)
|
||||
content.set_metadata(ContentMetadata.WIDTH, width)
|
||||
content.set_metadata(ContentMetadata.HEIGHT, height)
|
||||
elif content.type == ContentType.PICTURE:
|
||||
width, height = get_picture_metadata(content.location)
|
||||
content.set_metadata(ContentMetadata.WIDTH, width)
|
||||
content.set_metadata(ContentMetadata.HEIGHT, height)
|
||||
else:
|
||||
content.init_metadata()
|
||||
|
||||
return content.metadata
|
||||
|
||||
def delete(self, id: int) -> None:
|
||||
content = self.get(id)
|
||||
|
||||
@ -260,36 +230,20 @@ class ContentManager(ModelManager):
|
||||
var_external_url = self._variable_manager.get_one_by_name('external_url').as_string().strip().strip('/')
|
||||
location = content.location
|
||||
|
||||
if content.type == ContentType.YOUTUBE:
|
||||
location = content.location
|
||||
elif content.type == ContentType.TEXT:
|
||||
pass
|
||||
elif content.type == ContentType.COMPOSITION:
|
||||
if content.type == ContentType.EXTERNAL_STORAGE:
|
||||
var_external_storage_url = self._variable_manager.get_one_by_name('external_url_storage').as_string().strip().strip('/')
|
||||
port_ex_st = self._config_manager.map().get('port_http_external_storage')
|
||||
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:
|
||||
location = "{}/{}".format(
|
||||
var_external_url if len(var_external_url) > 0 else "",
|
||||
url_for(
|
||||
'serve_content_file',
|
||||
content_location=encode_uri_component(content.location),
|
||||
content_type=content.type.value,
|
||||
content_id=content.id
|
||||
).strip('/')
|
||||
var_external_storage_url if var_external_storage_url else 'http://{}:{}'.format(get_preferred_ip_address(), port_ex_st),
|
||||
content.location.strip('/')
|
||||
)
|
||||
elif content.type == ContentType.YOUTUBE:
|
||||
location = "https://www.youtube.com/watch?v={}".format(content.location)
|
||||
elif len(var_external_url) > 0 and content.has_file():
|
||||
location = "{}/{}".format(var_external_url, content.location)
|
||||
elif content.has_file():
|
||||
location = "/{}".format(content.location)
|
||||
elif content.type == ContentType.URL:
|
||||
location = 'http://' + content.location if content.location and not content.location.startswith('http') else content.location
|
||||
location = 'http://' + content.location if not content.location.startswith('http') else content.location
|
||||
|
||||
return location
|
||||
|
||||
def refresh_all_metadata(self):
|
||||
for content in self.get_all():
|
||||
self.update_form(
|
||||
id=content.id,
|
||||
metadata=self.set_metadata(content)
|
||||
)
|
||||
return location
|
||||
@ -3,7 +3,6 @@ import re
|
||||
import json
|
||||
import sqlite3
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from sqlite3 import Cursor
|
||||
from typing import Optional, Dict
|
||||
@ -214,11 +213,8 @@ class DatabaseManager:
|
||||
"DELETE FROM settings WHERE name = 'playlist_default_time_sync'",
|
||||
"DELETE FROM settings WHERE name = 'slide_animation_exit_effect'",
|
||||
"DELETE FROM settings WHERE name = 'playlist_enabled'",
|
||||
"DELETE FROM settings WHERE name = 'external_url_storage'",
|
||||
"UPDATE fleet_player_group SET slug = id WHERE slug = '' or slug is null",
|
||||
"UPDATE content SET uuid = id WHERE uuid = '' or uuid is null",
|
||||
"UPDATE slide SET uuid = id WHERE uuid = '' or uuid is null",
|
||||
"UPDATE user SET apikey = \'{}\' || id WHERE apikey = '' or apikey is null".format(str(uuid.uuid4())),
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
|
||||
@ -110,7 +110,7 @@ class FolderManager(ModelManager):
|
||||
for folder_id, edits in edits_folders.items():
|
||||
self._db.update_by_id(self.TABLE_NAME, folder_id, edits)
|
||||
|
||||
def get_folders(self, parent_id: Optional[int] = None) -> List[Folder]:
|
||||
def get_folders(self, parent_id: Optional[id] = None) -> List[Folder]:
|
||||
query = " 1=1 "
|
||||
|
||||
if parent_id:
|
||||
@ -267,9 +267,3 @@ class FolderManager(ModelManager):
|
||||
|
||||
def count_subfolders_for_folder(self, folder_id: int) -> int:
|
||||
return len(self.get_folders(parent_id=folder_id))
|
||||
|
||||
@staticmethod
|
||||
def is_root_drive(path: str):
|
||||
clean_path = path.strip('/')
|
||||
clean_root_path = FOLDER_ROOT_PATH.strip('/')
|
||||
return path == '/' or clean_path == clean_root_path
|
||||
|
||||
@ -81,7 +81,7 @@ class NodePlayerManager(ModelManager):
|
||||
for node_player_id, edits in edits_node_players.items():
|
||||
self._db.update_by_id(self.TABLE_NAME, node_player_id, edits)
|
||||
|
||||
def get_node_players(self, group_id: Optional[int] = None, folder_id: Optional[int] = None, sort: Optional[str] = None, ascending=False) -> List[NodePlayer]:
|
||||
def get_node_players(self, group_id: Optional[int] = None, folder_id: Optional[id] = None, sort: Optional[str] = None, ascending=False) -> List[NodePlayer]:
|
||||
query = " 1=1 "
|
||||
|
||||
if group_id:
|
||||
|
||||
@ -3,7 +3,6 @@ import os
|
||||
from typing import Dict, Optional, List, Tuple, Union
|
||||
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.model.enum.ContentType import ContentType
|
||||
from src.util.utils import get_optional_string, get_yt_video_id, slugify, slugify_next
|
||||
from src.manager.DatabaseManager import DatabaseManager
|
||||
from src.manager.SlideManager import SlideManager
|
||||
@ -70,17 +69,15 @@ class PlaylistManager(ModelManager):
|
||||
durations = self._db.execute_read_query("""
|
||||
SELECT
|
||||
playlist_id,
|
||||
ROUND(SUM(CASE
|
||||
SUM(CASE
|
||||
WHEN s.delegate_duration = 1 THEN c.duration
|
||||
WHEN c.type = '{}' THEN s.duration
|
||||
ELSE s.duration
|
||||
END)) AS total_duration
|
||||
END) AS total_duration
|
||||
FROM {} s
|
||||
LEFT JOIN {} c ON c.id = s.content_id
|
||||
WHERE cron_schedule IS NULL {} AND s.enabled is TRUE
|
||||
WHERE cron_schedule IS NULL {}
|
||||
GROUP BY playlist_id;
|
||||
""".format(
|
||||
ContentType.EXTERNAL_STORAGE.value,
|
||||
SlideManager.TABLE_NAME,
|
||||
ContentManager.TABLE_NAME,
|
||||
"{}".format(
|
||||
@ -142,9 +139,7 @@ GROUP BY playlist_id;
|
||||
return playlist
|
||||
|
||||
def pre_update(self, playlist: Dict) -> Dict:
|
||||
if 'slug' in playlist:
|
||||
playlist = self.slugify(playlist)
|
||||
|
||||
playlist = self.slugify(playlist)
|
||||
self.user_manager.track_user_on_update(playlist)
|
||||
return playlist
|
||||
|
||||
@ -163,22 +158,18 @@ GROUP BY playlist_id;
|
||||
def post_delete(self, playlist_id: str) -> str:
|
||||
return playlist_id
|
||||
|
||||
def update_form(self, id: int, name: Optional[str] = None, time_sync: Optional[bool] = None, enabled: Optional[bool] = None, fallback: Optional[bool] = None) -> None:
|
||||
def update_form(self, id: int, name: Optional[str] = None, time_sync: Optional[bool] = None, enabled: Optional[bool] = None) -> None:
|
||||
playlist = self.get(id)
|
||||
|
||||
if not playlist:
|
||||
return
|
||||
|
||||
form = {
|
||||
"name": name if isinstance(name, str) else playlist.name,
|
||||
"time_sync": time_sync if isinstance(time_sync, bool) else playlist.time_sync,
|
||||
"enabled": enabled if isinstance(enabled, bool) else playlist.enabled,
|
||||
"fallback": fallback if isinstance(fallback, bool) else playlist.fallback,
|
||||
"name": name,
|
||||
"time_sync": time_sync if isinstance(time_sync, bool) else slide.time_sync,
|
||||
"enabled": enabled if isinstance(enabled, bool) else slide.enabled,
|
||||
}
|
||||
|
||||
if name != playlist.name:
|
||||
form["slug"] = True
|
||||
|
||||
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))
|
||||
|
||||
if playlist.fallback and not enabled:
|
||||
@ -187,7 +178,7 @@ GROUP BY playlist_id;
|
||||
self.post_update(id)
|
||||
|
||||
def check_and_set_fallback(self):
|
||||
if self.count_fallbacks() == 0:
|
||||
if len(self.get_by("fallback = 1")) == 0:
|
||||
self.set_fallback()
|
||||
|
||||
def set_fallback(self, playlist_id: Optional[int] = 0) -> None:
|
||||
@ -227,8 +218,3 @@ GROUP BY playlist_id;
|
||||
def to_dict(self, playlists: List[Playlist]) -> List[Dict]:
|
||||
return [playlist.to_dict() for playlist in playlists]
|
||||
|
||||
def count_all(self):
|
||||
return len(self.get_all())
|
||||
|
||||
def count_fallbacks(self):
|
||||
return len(self.get_by("fallback = 1"))
|
||||
|
||||
@ -16,7 +16,6 @@ class SlideManager(ModelManager):
|
||||
|
||||
TABLE_NAME = "slides"
|
||||
TABLE_MODEL = [
|
||||
"uuid CHAR(255)",
|
||||
"enabled INTEGER DEFAULT 0",
|
||||
"delegate_duration INTEGER DEFAULT 0",
|
||||
"is_notification INTEGER DEFAULT 0",
|
||||
@ -137,19 +136,18 @@ class SlideManager(ModelManager):
|
||||
for slide_id, slide_position in positions.items():
|
||||
self._db.update_by_id(self.TABLE_NAME, slide_id, {"position": slide_position})
|
||||
|
||||
def update_form(self, id: int, duration: Optional[int] = None, content_id: Optional[int] = None, delegate_duration: Optional[bool] = None, is_notification: Optional[bool] = None, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', enabled: Optional[bool] = None, position: Optional[int] = None) -> Optional[Slide]:
|
||||
def update_form(self, id: int, duration: Optional[int] = None, content_id: Optional[int] = None, delegate_duration: Optional[bool] = None, is_notification: bool = False, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', enabled: Optional[bool] = None) -> Optional[Slide]:
|
||||
slide = self.get(id)
|
||||
|
||||
if not slide:
|
||||
return
|
||||
|
||||
form = {
|
||||
"duration": duration if duration and int(duration) >= 0 else slide.duration,
|
||||
"content_id": content_id if isinstance(content_id, int) else slide.content_id,
|
||||
"position": position if isinstance(position, int) else slide.position,
|
||||
"duration": duration if duration else slide.duration,
|
||||
"content_id": content_id if content_id else slide.content_id,
|
||||
"enabled": enabled if isinstance(enabled, bool) else slide.enabled,
|
||||
"delegate_duration": delegate_duration if isinstance(delegate_duration, bool) else slide.delegate_duration,
|
||||
"is_notification": is_notification if isinstance(is_notification, bool) else slide.is_notification,
|
||||
"is_notification": True if is_notification else False,
|
||||
"cron_schedule": get_optional_string(cron_schedule),
|
||||
"cron_schedule_end": get_optional_string(cron_schedule_end)
|
||||
}
|
||||
@ -158,7 +156,7 @@ class SlideManager(ModelManager):
|
||||
self.post_update(id)
|
||||
return self.get(id)
|
||||
|
||||
def add_form(self, slide: Union[Slide, Dict]) -> Slide:
|
||||
def add_form(self, slide: Union[Slide, Dict]) -> None:
|
||||
form = slide
|
||||
|
||||
if not isinstance(slide, dict):
|
||||
@ -167,7 +165,6 @@ class SlideManager(ModelManager):
|
||||
|
||||
self._db.add(self.TABLE_NAME, self.pre_add(form))
|
||||
self.post_add(slide.id)
|
||||
return self.get_one_by(query="uuid = '{}'".format(slide.uuid))
|
||||
|
||||
def delete(self, id: int) -> None:
|
||||
slide = self.get(id)
|
||||
|
||||
@ -15,7 +15,6 @@ class UserManager:
|
||||
TABLE_MODEL = [
|
||||
"username CHAR(255)",
|
||||
"password CHAR(255)",
|
||||
"apikey CHAR(255)",
|
||||
"enabled INTEGER DEFAULT 1",
|
||||
"created_by CHAR(255)",
|
||||
"updated_by CHAR(255)",
|
||||
@ -80,21 +79,8 @@ class UserManager:
|
||||
|
||||
return self.hydrate_object(object)
|
||||
|
||||
def get_one_by_username(self, username: str, enabled: Optional[bool] = None) -> Optional[User]:
|
||||
query = " username = ? "
|
||||
|
||||
if enabled:
|
||||
query = "{} {}".format(query, "AND enabled = {}".format(int(enabled)))
|
||||
|
||||
return self.get_one_by(query=query, values={"username": username})
|
||||
|
||||
def get_one_by_apikey(self, apikey: str, enabled: Optional[bool] = None) -> Optional[User]:
|
||||
query = " apikey = ? "
|
||||
|
||||
if enabled:
|
||||
query = "{} {}".format(query, "AND enabled = {}".format(int(enabled)))
|
||||
|
||||
return self.get_one_by(query=query, values={"apikey": apikey})
|
||||
def get_one_by_username(self, username: str, enabled: bool = None) -> Optional[User]:
|
||||
return self.get_one_by("username = '{}' and (enabled is null or enabled = {})".format(username, int(enabled)))
|
||||
|
||||
def count_all_enabled(self):
|
||||
return len(self.get_by("enabled = 1"))
|
||||
@ -218,14 +204,13 @@ class UserManager:
|
||||
user_id = self.get_logged_user("id")
|
||||
now = time.time()
|
||||
|
||||
if user_id:
|
||||
if 'created_by' not in object or not object['created_by']:
|
||||
object["created_by"] = user_id
|
||||
edits['created_by'] = object['created_by']
|
||||
if 'created_by' not in object or not object['created_by']:
|
||||
object["created_by"] = user_id
|
||||
edits['created_by'] = object['created_by']
|
||||
|
||||
if 'updated_by' not in object or not object['updated_by']:
|
||||
object["updated_by"] = user_id
|
||||
edits['updated_by'] = object['updated_by']
|
||||
if 'updated_by' not in object or not object['updated_by']:
|
||||
object["updated_by"] = user_id
|
||||
edits['updated_by'] = object['updated_by']
|
||||
|
||||
if 'created_at' not in object or not object['created_at']:
|
||||
object["created_at"] = now
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user