Added controls for projector elements
countdowns, messages, scale/scroll/edit current slide Use global serverOffset. Fixed #1645 (wrong time of projector clock). Show countdown description on projector slide.
This commit is contained in:
parent
0c9d4b2627
commit
dda16af532
@ -25,6 +25,7 @@ Users:
|
||||
- Used authentication frontend via AngularJS.
|
||||
Other:
|
||||
- New OpenSlides logo.
|
||||
- Added multiple countdown support.
|
||||
- Changed supported Python version to >= 3.3.
|
||||
- Used Django 1.7 as lowest requirement.
|
||||
- Added Django's application configuration. Refactored loading of signals
|
||||
|
36
openslides/core/migrations/0007_clear_default_countdown.py
Normal file
36
openslides/core/migrations/0007_clear_default_countdown.py
Normal file
@ -0,0 +1,36 @@
|
||||
import uuid
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def clear_all_and_make_it_new_2(apps, schema_editor):
|
||||
"""
|
||||
Clear all projector elements and them write new.
|
||||
"""
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
Projector = apps.get_model('core', 'Projector')
|
||||
projector = Projector.objects.get()
|
||||
projector.config = {}
|
||||
projector.config[uuid.uuid4().hex] = {
|
||||
'name': 'core/clock',
|
||||
'stable': True}
|
||||
projector.config[uuid.uuid4().hex] = {
|
||||
'name': 'core/customslide',
|
||||
'id': 1} # TODO: Use ID from model here. Do not guess.
|
||||
projector.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_auto_20150914_2232'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=clear_all_and_make_it_new_2,
|
||||
reverse_code=None,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
@ -82,6 +82,7 @@ class Projector(RESTModelMixin, models.Model):
|
||||
for key, value in self.config.items():
|
||||
# Use a copy here not to change the origin value in the config field.
|
||||
result[key] = value.copy()
|
||||
result[key]['uuid'] = key
|
||||
element = elements.get(value['name'])
|
||||
if element is None:
|
||||
result[key]['error'] = _('Projector element does not exist.')
|
||||
|
@ -23,6 +23,32 @@ body {
|
||||
border: 2px dashed #bed2db;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.countdown.panel, .message.panel {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.countdown .panel-heading {
|
||||
padding: 3px 15px;
|
||||
}
|
||||
.countdown .panel-body {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
.message .panel-body {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
.countdown_timer {
|
||||
font-size: 2.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.countdown .editicon, .message .editicon {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.vcenter {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.notNull {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* TODO: used by ng-fab-forms */
|
||||
.validation-success {
|
||||
@ -184,7 +210,7 @@ a:hover {
|
||||
}
|
||||
|
||||
/* List tables */
|
||||
th.sortable:hover, tr.pointer:hover {
|
||||
th.sortable:hover, tr.pointer:hover, .pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -245,7 +271,7 @@ div.import > div > input[type="text"] {
|
||||
tr.offline td, li.offline {
|
||||
background-color: #EAEAEA !important;
|
||||
}
|
||||
tr.activeline td, li.activeline {
|
||||
tr.activeline td, li.activeline, .projected {
|
||||
background-color: #bed4de;
|
||||
}
|
||||
.nopadding {
|
||||
|
@ -130,7 +130,28 @@ hr {
|
||||
|
||||
|
||||
/*** Overlay ***/
|
||||
#overlay_transparent {
|
||||
.countdown {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
top: 0;
|
||||
right: 0px;
|
||||
padding: 19px 20px 5px 19px;
|
||||
min-height: 72px;
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-radius: 0.2em 0 0 0.2em;
|
||||
z-index: 200;
|
||||
}
|
||||
.countdown .description {
|
||||
font-weight: normal;
|
||||
font-size: 0.2em;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.countdown.negative {
|
||||
color: #CC0000;
|
||||
}
|
||||
.message_background {
|
||||
background-color: #777777;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
@ -138,23 +159,9 @@ hr {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 200;
|
||||
}
|
||||
#overlay_countdown_inner {
|
||||
position: fixed;
|
||||
right: 40px;
|
||||
height: 30px;
|
||||
width: 215px;
|
||||
margin: 0;
|
||||
top: 0;
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-radius: 0 0 0.2em 0.2em;
|
||||
}
|
||||
#overlay_countdown_inner.negative {
|
||||
color: #CC0000;
|
||||
}
|
||||
#overlay_message_inner {
|
||||
.message {
|
||||
position: fixed;
|
||||
top: 35%;
|
||||
left: 10%;
|
||||
@ -165,6 +172,7 @@ hr {
|
||||
font-size: 2.75em;
|
||||
padding: 0.2em 0;
|
||||
line-height: normal !important;
|
||||
z-index: 201;
|
||||
}
|
||||
|
||||
|
||||
|
@ -77,9 +77,10 @@ angular.module('OpenSlidesApp.core', [
|
||||
|
||||
.factory('loadGlobalData', [
|
||||
'$rootScope',
|
||||
'$http',
|
||||
'Config',
|
||||
'Projector',
|
||||
function ($rootScope, Config, Projector) {
|
||||
function ($rootScope, $http, Config, Projector) {
|
||||
return function () {
|
||||
// Puts the config object into each scope.
|
||||
Config.findAll().then(function() {
|
||||
@ -96,6 +97,11 @@ angular.module('OpenSlidesApp.core', [
|
||||
|
||||
// Loads all projector data
|
||||
Projector.findAll();
|
||||
|
||||
// Loads server time and calculates server offset
|
||||
$http.get('/core/servertime/').then(function(data) {
|
||||
$rootScope.serverOffset = Math.floor( Date.now() / 1000 - data.data );
|
||||
});
|
||||
}
|
||||
}
|
||||
])
|
||||
@ -176,6 +182,34 @@ angular.module('OpenSlidesApp.core', [
|
||||
});
|
||||
}])
|
||||
|
||||
/* Converts number of seconds into string "hh:mm:ss" or "mm:ss" */
|
||||
.filter('osSecondsToTime', [
|
||||
function () {
|
||||
return function (totalseconds) {
|
||||
var time;
|
||||
var total = Math.abs(totalseconds);
|
||||
if (parseInt(totalseconds)) {
|
||||
var hh = Math.floor(total / 3600);
|
||||
var mm = Math.floor(total % 3600 / 60);
|
||||
var ss = Math.floor(total % 60);
|
||||
var zero = "0";
|
||||
// Add leading "0" for double digit values
|
||||
hh = (zero+hh).slice(-2);
|
||||
mm = (zero+mm).slice(-2);
|
||||
ss = (zero+ss).slice(-2);
|
||||
if (hh == "00")
|
||||
time = mm + ':' + ss;
|
||||
else
|
||||
time = hh + ":" + mm + ":" + ss;
|
||||
if (totalseconds < 0)
|
||||
time = "-"+time;
|
||||
} else {
|
||||
time = "--:--";
|
||||
}
|
||||
return time;
|
||||
};
|
||||
}
|
||||
])
|
||||
// Make sure that the DS factories are loaded by making them a dependency
|
||||
.run(['Projector', 'Config', 'Tag', 'Customslide', function(Projector, Config, Tag, Customslide){}]);
|
||||
|
||||
@ -572,12 +606,16 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
])
|
||||
|
||||
// Version Controller
|
||||
.controller('VersionCtrl', function ($scope, $http) {
|
||||
$http.get('/core/version/').success(function(data) {
|
||||
$scope.core_version = data.openslides_version;
|
||||
$scope.plugins = data.plugins;
|
||||
});
|
||||
})
|
||||
.controller('VersionCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
function ($scope, $http) {
|
||||
$http.get('/core/version/').success(function(data) {
|
||||
$scope.core_version = data.openslides_version;
|
||||
$scope.plugins = data.plugins;
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
// Config Controller
|
||||
.controller('ConfigCtrl', function($scope, Config, configOption) {
|
||||
@ -592,33 +630,231 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
})
|
||||
|
||||
// Customslide Controller
|
||||
.controller('CustomslideListCtrl', function($scope, Customslide) {
|
||||
Customslide.bindAll({}, $scope, 'customslides');
|
||||
.controller('CustomslideListCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'Customslide',
|
||||
function($scope, $http, Customslide) {
|
||||
Customslide.bindAll({}, $scope, 'customslides');
|
||||
|
||||
// setup table sorting
|
||||
$scope.sortColumn = 'title';
|
||||
$scope.reverse = false;
|
||||
// function to sort by clicked column
|
||||
$scope.toggleSort = function ( column ) {
|
||||
if ( $scope.sortColumn === column ) {
|
||||
$scope.reverse = !$scope.reverse;
|
||||
}
|
||||
$scope.sortColumn = column;
|
||||
};
|
||||
|
||||
// save changed customslide
|
||||
$scope.save = function (customslide) {
|
||||
Customslide.save(customslide);
|
||||
};
|
||||
$scope.delete = function (customslide) {
|
||||
//TODO: add confirm message
|
||||
Customslide.destroy(customslide.id).then(
|
||||
function(success) {
|
||||
//TODO: success message
|
||||
// setup table sorting
|
||||
$scope.sortColumn = 'title';
|
||||
$scope.reverse = false;
|
||||
// function to sort by clicked column
|
||||
$scope.toggleSort = function ( column ) {
|
||||
if ( $scope.sortColumn === column ) {
|
||||
$scope.reverse = !$scope.reverse;
|
||||
}
|
||||
);
|
||||
};
|
||||
})
|
||||
$scope.sortColumn = column;
|
||||
};
|
||||
|
||||
// save changed customslide
|
||||
$scope.save = function (customslide) {
|
||||
Customslide.save(customslide);
|
||||
};
|
||||
$scope.delete = function (customslide) {
|
||||
//TODO: add confirm message
|
||||
Customslide.destroy(customslide.id).then(
|
||||
function(success) {
|
||||
//TODO: success message
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// Projector Control Controller
|
||||
.controller('ProjectorControlCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'$interval',
|
||||
'$state',
|
||||
'Config',
|
||||
'Projector',
|
||||
function($scope, $http, $interval, $state, Config, Projector) {
|
||||
// bind projector elements to the scope, update after projector changed
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified(1);
|
||||
}, function () {
|
||||
// stop ALL interval timer
|
||||
for (var i=0; i<$scope.countdowns.length; i++) {
|
||||
if ( $scope.countdowns[i].interval ) {
|
||||
$interval.cancel($scope.countdowns[i].interval);
|
||||
}
|
||||
}
|
||||
// rebuild all variables after projector update
|
||||
$scope.rebuildAllElements();
|
||||
});
|
||||
$scope.$on('$destroy', function() {
|
||||
// Cancel all intervals if the controller is destroyed
|
||||
for (var i=0; i<$scope.countdowns.length; i++) {
|
||||
if ( $scope.countdowns[i].interval ) {
|
||||
$interval.cancel($scope.countdowns[i].interval);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// *** countdown functions ***
|
||||
$scope.calculateCountdownTime = function (countdown) {
|
||||
countdown.seconds = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
||||
}
|
||||
$scope.rebuildAllElements = function () {
|
||||
$scope.countdowns = [];
|
||||
$scope.messages = [];
|
||||
// iterate via all projector elements and catch all countdowns and messages
|
||||
$.each(Projector.get(1).elements, function(key, value) {
|
||||
if (value.name == 'core/countdown') {
|
||||
$scope.countdowns.push(value);
|
||||
if (value.status == "running") {
|
||||
// calculate remaining seconds directly because interval starts with 1 second delay
|
||||
$scope.calculateCountdownTime(value);
|
||||
// start interval timer (every second)
|
||||
value.interval = $interval( function() { $scope.calculateCountdownTime(value); }, 1000);
|
||||
} else {
|
||||
value.seconds = value.countdown_time;
|
||||
}
|
||||
}
|
||||
if (value.name == 'core/message') {
|
||||
$scope.messages.push(value);
|
||||
}
|
||||
});
|
||||
$scope.scrollLevel = Projector.get(1).scroll;
|
||||
$scope.scaleLevel = Projector.get(1).scale;
|
||||
}
|
||||
|
||||
// get initial values for $scope.countdowns, $scope.messages, $scope.scrollLevel
|
||||
// and $scope.scaleLevel (after page reload)
|
||||
$scope.rebuildAllElements();
|
||||
|
||||
$scope.addCountdown = function () {
|
||||
var defaultvalue = parseInt(Config.get('projector_default_countdown').value);
|
||||
$http.post('/rest/core/projector/1/activate_elements/', [{
|
||||
name: 'core/countdown',
|
||||
status: 'stop',
|
||||
visible: false,
|
||||
index: $scope.countdowns.length,
|
||||
countdown_time: defaultvalue,
|
||||
default: defaultvalue,
|
||||
stable: true
|
||||
}]);
|
||||
};
|
||||
$scope.removeCountdown = function (countdown) {
|
||||
var data = {};
|
||||
var delta = 0;
|
||||
// rebuild index for all countdowns after the selected (deleted) countdown
|
||||
for (var i=0; i<$scope.countdowns.length; i++) {
|
||||
if ( $scope.countdowns[i].uuid == countdown.uuid ) {
|
||||
delta = 1;
|
||||
} else if (delta > 0) {
|
||||
data[$scope.countdowns[i].uuid] = { "index": i - delta };
|
||||
}
|
||||
}
|
||||
$http.post('/rest/core/projector/1/deactivate_elements/', [countdown.uuid]);
|
||||
if (Object.keys(data).length > 0) {
|
||||
$http.post('/rest/core/projector/1/update_elements/', data);
|
||||
}
|
||||
};
|
||||
$scope.showCountdown = function (countdown) {
|
||||
var data = {};
|
||||
data[countdown.uuid] = { "visible": !countdown.visible };
|
||||
$http.post('/rest/core/projector/1/update_elements/', data);
|
||||
};
|
||||
$scope.editCountdown = function (countdown) {
|
||||
var data = {};
|
||||
data[countdown.uuid] = {
|
||||
"description": countdown.description,
|
||||
"default": parseInt(countdown.default)
|
||||
};
|
||||
if (countdown.status == "stop") {
|
||||
data[countdown.uuid].countdown_time = parseInt(countdown.default);
|
||||
}
|
||||
$http.post('/rest/core/projector/1/update_elements/', data);
|
||||
};
|
||||
$scope.startCountdown = function (countdown) {
|
||||
var data = {};
|
||||
// calculate end point of countdown (in seconds!)
|
||||
var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time;
|
||||
data[countdown.uuid] = {
|
||||
"status": "running",
|
||||
"countdown_time": endTimestamp
|
||||
};
|
||||
$http.post('/rest/core/projector/1/update_elements/', data);
|
||||
};
|
||||
$scope.stopCountdown = function (countdown) {
|
||||
var data = {};
|
||||
// calculate rest duration of countdown (in seconds!)
|
||||
var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
||||
data[countdown.uuid] = {
|
||||
"status": "stop",
|
||||
"countdown_time": newDuration
|
||||
};
|
||||
$http.post('/rest/core/projector/1/update_elements/', data);
|
||||
};
|
||||
$scope.resetCountdown = function (countdown) {
|
||||
var data = {};
|
||||
data[countdown.uuid] = {
|
||||
"status": "stop",
|
||||
"countdown_time": countdown.default,
|
||||
};
|
||||
$http.post('/rest/core/projector/1/update_elements/', data);
|
||||
};
|
||||
|
||||
// *** message functions ***
|
||||
$scope.addMessage = function () {
|
||||
$http.post('/rest/core/projector/1/activate_elements/', [{
|
||||
name: 'core/message',
|
||||
visible: false,
|
||||
index: $scope.messages.length,
|
||||
message: '',
|
||||
stable: true
|
||||
}]);
|
||||
};
|
||||
$scope.removeMessage = function (message) {
|
||||
$http.post('/rest/core/projector/1/deactivate_elements/', [message.uuid]);
|
||||
};
|
||||
$scope.showMessage = function (message) {
|
||||
var data = {};
|
||||
// if current message is activated, deactivate all other messages
|
||||
if ( !message.visible ) {
|
||||
for (var i=0; i<$scope.messages.length; i++) {
|
||||
if ( $scope.messages[i].uuid == message.uuid ) {
|
||||
data[$scope.messages[i].uuid] = { "visible": true };
|
||||
} else {
|
||||
data[$scope.messages[i].uuid] = { "visible": false };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data[message.uuid] = { "visible": false };
|
||||
}
|
||||
$http.post('/rest/core/projector/1/update_elements/', data);
|
||||
};
|
||||
$scope.editMessage = function (message) {
|
||||
var data = {};
|
||||
data[message.uuid] = {
|
||||
"message": message.message,
|
||||
};
|
||||
$http.post('/rest/core/projector/1/update_elements/', data);
|
||||
message.editMessageFlag = false;
|
||||
};
|
||||
|
||||
// *** projector controls ***
|
||||
$scope.scrollLevel = Projector.get(1).scroll;
|
||||
$scope.scaleLevel = Projector.get(1).scale;
|
||||
$scope.controlProjector = function (action, direction) {
|
||||
$http.post('/rest/core/projector/1/control_view/', {"action": action, "direction": direction});
|
||||
};
|
||||
$scope.editCurrentSlide = function () {
|
||||
$.each(Projector.get(1).elements, function(key, value) {
|
||||
if (value.name != 'core/clock' &&
|
||||
value.name != 'core/countdown' &&
|
||||
value.name != 'core/message' ) {
|
||||
$state.go(value.name.replace('/', '.')+'.detail.update', {id: value.id });
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('CustomslideDetailCtrl', function($scope, Customslide, customslide) {
|
||||
Customslide.bindOne(customslide.id, $scope, 'customslide');
|
||||
@ -748,13 +984,14 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
||||
slidesProvider.registerSlide('core/clock', {
|
||||
template: 'static/templates/core/slide_clock.html',
|
||||
});
|
||||
})
|
||||
|
||||
.filter('osServertime',function() {
|
||||
return function(serverOffset) {
|
||||
var date = new Date();
|
||||
return date.setTime(date.getTime() - serverOffset);
|
||||
};
|
||||
slidesProvider.registerSlide('core/countdown', {
|
||||
template: 'static/templates/core/slide_countdown.html',
|
||||
});
|
||||
|
||||
slidesProvider.registerSlide('core/message', {
|
||||
template: 'static/templates/core/slide_message.html',
|
||||
});
|
||||
})
|
||||
|
||||
.controller('ProjectorCtrl', function($scope, Projector, slides) {
|
||||
@ -770,22 +1007,70 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
||||
console.error("Error for slide " + element.name + ": " + element.error)
|
||||
}
|
||||
});
|
||||
$scope.scroll = -10 * Projector.get(1).scroll;
|
||||
$scope.scale = 100 + 20 * Projector.get(1).scale;
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
.controller('SlideCustomSlideCtrl', function($scope, Customslide) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
var id = $scope.element.id;
|
||||
Customslide.find(id);
|
||||
Customslide.bindOne(id, $scope, 'customslide');
|
||||
})
|
||||
.controller('SlideCustomSlideCtrl', [
|
||||
'$scope',
|
||||
'Customslide',
|
||||
function($scope, Customslide) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
var id = $scope.element.id;
|
||||
Customslide.find(id);
|
||||
Customslide.bindOne(id, $scope, 'customslide');
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideClockCtrl', function($scope) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
$scope.serverOffset = Date.parse(new Date().toUTCString()) - $scope.element.context.server_time;
|
||||
});
|
||||
.controller('SlideClockCtrl', [
|
||||
'$scope',
|
||||
function($scope) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
$scope.servertime = ( Date.now() / 1000 - $scope.serverOffset ) * 1000;
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideCountdownCtrl', [
|
||||
'$scope',
|
||||
'$interval',
|
||||
function($scope, $interval) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
||||
$scope.status = $scope.element.status;
|
||||
$scope.visible = $scope.element.visible;
|
||||
$scope.index = $scope.element.index;
|
||||
$scope.description = $scope.element.description;
|
||||
// start interval timer if countdown status is running
|
||||
var interval;
|
||||
if ($scope.status == "running") {
|
||||
interval = $interval( function() {
|
||||
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
||||
}, 1000);
|
||||
} else {
|
||||
$scope.seconds = $scope.element.countdown_time;
|
||||
}
|
||||
$scope.$on('$destroy', function() {
|
||||
// Cancel the interval if the controller is destroyed
|
||||
$interval.cancel(interval);
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideMessageCtrl', [
|
||||
'$scope',
|
||||
function($scope) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
$scope.message = $scope.element.message;
|
||||
$scope.visible = $scope.element.visible;
|
||||
}
|
||||
]);
|
||||
|
@ -48,15 +48,8 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<!-- projector live view -->
|
||||
<div class="well">
|
||||
<h4 translate>Projector live view</h4>
|
||||
<a ui-sref="projector" target="_blank">
|
||||
<div id="iframewrapper">
|
||||
<iframe id="iframe" src="/projector" frameborder="0"></iframe>
|
||||
<div id="iframeoverlay"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div ng-include src="'static/templates/core/projector-controls.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
177
openslides/core/static/templates/core/projector-controls.html
Normal file
177
openslides/core/static/templates/core/projector-controls.html
Normal file
@ -0,0 +1,177 @@
|
||||
<!-- projector live view -->
|
||||
<h3 translate>Projector</h4>
|
||||
<a ui-sref="projector" target="_blank">
|
||||
<div id="iframewrapper">
|
||||
<iframe id="iframe" src="/projector" frameborder="0"></iframe>
|
||||
<div id="iframeoverlay"></div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div ng-controller="ProjectorControlCtrl">
|
||||
<!-- projector control buttons -->
|
||||
<p os-perms="core.can_manage_projector">
|
||||
<a ng-click="editCurrentSlide()"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Edit current slide' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
|
||||
<a ng-click="controlProjector('scale', 'down')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Smaller' | translate}}">
|
||||
<i class="fa fa-search-minus"></i>
|
||||
</a>
|
||||
<a ng-click="controlProjector('scale', 'up')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Bigger' | translate}}">
|
||||
<i class="fa fa-search-plus"></i>
|
||||
</a>
|
||||
<span ng-class="{ 'notNull': scaleLevel != 0 }">{{ scaleLevel }}</span>
|
||||
|
||||
<a ng-click="controlProjector('scroll', 'down')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Scroll up' | translate}}">
|
||||
<i class="fa fa-arrow-up"></i>
|
||||
</a>
|
||||
<a ng-click="controlProjector('scroll', 'up')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Scroll down' | translate}}">
|
||||
<i class="fa fa-arrow-down"></i>
|
||||
</a>
|
||||
<a ng-click="controlProjector('scroll', 'reset')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Reset scrolling' | translate}}">
|
||||
<i class="fa fa-undo"></i>
|
||||
</a>
|
||||
<span ng-class="{ 'notNull': scrollLevel != 0 }">{{ scrollLevel }}</span>
|
||||
</p>
|
||||
|
||||
<!-- countdowns -->
|
||||
<div os-perms-lite="core.can_manage_projector">
|
||||
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="{{countdown.uuid}}" class="countdown panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span ng-if="countdown.description">{{ countdown.description }}</span>
|
||||
<span ng-if="!countdown.description">Countdown {{ countdown.index +1 }}</span>
|
||||
<!-- remove countdown button -->
|
||||
<button type="button" class="close"
|
||||
ng-click="removeCountdown(countdown)"
|
||||
title="{{ 'Remove countdown' | translate}}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<!-- edit countdown button -->
|
||||
<button ng-show="countdown.status=='stop'"
|
||||
type="button" class="close editicon"
|
||||
ng-click="editCountdownFlag=true;"
|
||||
title="{{ 'Edit countdown' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body"
|
||||
ng-class="{ 'projected': countdown.visible }">
|
||||
<!-- project countdown button -->
|
||||
<a class="btn btn-default btn-sm"
|
||||
ng-model="countdown.visible"
|
||||
ng-click="showCountdown(countdown)"
|
||||
ng-class="{ 'btn-primary': countdown.visible }"
|
||||
title="{{ 'Project countdown' | translate }}">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</a>
|
||||
|
||||
<!-- countdown controls -->
|
||||
<a class="btn btn-default vcenter"
|
||||
ng-click="resetCountdown(countdown)"
|
||||
ng-class="{ 'disabled': countdown.status == 'stop' && countdown.default == countdown.countdown_time }"
|
||||
title="{{ 'Reset countdown' | translate}}">
|
||||
<i class="fa fa-stop"></i>
|
||||
</a>
|
||||
<a ng-if="countdown.status=='stop'" class="btn btn-default vcenter"
|
||||
ng-click="startCountdown(countdown)"
|
||||
title="{{ 'Start' | translate}}">
|
||||
<i class="fa fa-play"></i>
|
||||
<i ng-if="countdown.status=='running'" class="fa fa-pause"></i>
|
||||
</a>
|
||||
<a ng-if="countdown.status=='running'" class="btn btn-default vcenter"
|
||||
ng-click="stopCountdown(countdown)"
|
||||
title="{{ 'Pause' | translate}}">
|
||||
<i class="fa fa-pause"></i>
|
||||
</a>
|
||||
<span ng-if="!editTime" class="countdown_timer vcenter"
|
||||
ng-class="{ 'negative': countdown.seconds < 0 }">
|
||||
{{ countdown.seconds | osSecondsToTime }}
|
||||
</span>
|
||||
<!-- edit countdown form -->
|
||||
<form ng-show="editCountdownFlag" ng-submit="editCountdown(countdown)">
|
||||
<div class="form-group">
|
||||
<label translate>Description</label>
|
||||
<input ng-model="countdown.description" type="text" class="form-control input-sm">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>Start time</label>
|
||||
<input ng-model="countdown.default" type="number" class="form-control input-sm">
|
||||
</div>
|
||||
<button type="submit"
|
||||
title="{{ 'Save' | translate}}"
|
||||
class="btn btn-sm btn-primary">
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button ng-click="editCountdownFlag=false;"
|
||||
title="{{ 'Cancel' | translate}}"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add countdown button -->
|
||||
<a ng-click="addCountdown()"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Add countdown' | translate}}">
|
||||
<i class="fa fa-plus"></i> <translate>Add new countdown</translate>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- messages -->
|
||||
<div os-perms-lite="core.can_manage_projector">
|
||||
<h3 translate>Messages</h3>
|
||||
<div ng-repeat="message in messages | orderBy: 'index'" id="{{message.uuid}}" class="message panel panel-default">
|
||||
<div class="panel-body"
|
||||
ng-class="{ 'projected': message.visible }">
|
||||
<!-- project message button -->
|
||||
<a class="btn btn-default btn-sm"
|
||||
ng-model="message.visible"
|
||||
ng-click="showMessage(message)"
|
||||
ng-class="{ 'btn-primary': message.visible }"
|
||||
title="{{ 'Project message' | translate }}">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</a>
|
||||
|
||||
{{ message.message }}
|
||||
<!-- remove message button -->
|
||||
<button type="button" class="close"
|
||||
ng-click="removeMessage(message)"
|
||||
title="{{ 'Remove message' | translate}}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<button type="button" class="close editicon"
|
||||
ng-click="editMessageFlag=true;"
|
||||
title="{{ 'Edit message' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<div ng-if="editMessageFlag" class="input-group">
|
||||
<input ng-model="message.message" type="text" class="form-control input-sm">
|
||||
<a ng-click="editMessage(message)"
|
||||
title="{{ 'Save' | translate}}"
|
||||
class="btn btn-sm btn-primary input-group-addon">
|
||||
<i class="fa fa-check"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add message button -->
|
||||
<a ng-click="addMessage()"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Add message' | translate}}">
|
||||
<i class="fa fa-plus"></i> <translate>Add new message</translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
<div ng-controller="SlideClockCtrl" id="currentTime">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
{{ serverOffset | osServertime | date:'HH:mm' }}
|
||||
{{ servertime | date:'HH:mm' }}
|
||||
</div>
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div ng-controller="SlideCountdownCtrl">
|
||||
<div ng-if="visible">
|
||||
<div class="countdown well" style="margin-top: calc({{index}}*100px);">
|
||||
{{ seconds | osSecondsToTime}}
|
||||
<div class="description">{{ description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
<div ng-controller="SlideCustomSlideCtrl" class="content">
|
||||
<div ng-controller="SlideCustomSlideCtrl" class="content scrollcontent">
|
||||
<h1>{{ customslide.title }}</h1>
|
||||
<div class="white-space-pre-line">{{ customslide.text }}</div>
|
||||
</div>
|
||||
|
4
openslides/core/static/templates/core/slide_message.html
Normal file
4
openslides/core/static/templates/core/slide_message.html
Normal file
@ -0,0 +1,4 @@
|
||||
<div ng-controller="SlideMessageCtrl">
|
||||
<div ng-if="visible" class="message_background"></div>
|
||||
<div ng-if="visible" class="message well">{{ message }}</div>
|
||||
</div>
|
@ -54,6 +54,7 @@
|
||||
<!-- Login dialog (modal) -->
|
||||
<div ng-controller="LoginFormCtrl" ng-if="!operator.isAuthenticated()">
|
||||
<script type="text/ng-template" id="LoginForm.html">
|
||||
<form ng-submit="login(username, password)">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" translate>Please sign in!</h3>
|
||||
</div>
|
||||
@ -62,7 +63,7 @@
|
||||
<strong translate>Username or password is not correct.</strong>
|
||||
<div class="input-group form-group">
|
||||
<div class="input-group-addon"><i class="fa fa-user"></i></div>
|
||||
<input type="text" ng-model="username" class="form-control input-lg"
|
||||
<input os-focus-me type="text" ng-model="username" class="form-control input-lg"
|
||||
placeholder="{{ 'Username' | translate }}">
|
||||
</div>
|
||||
<div class="input-group form-group">
|
||||
@ -73,8 +74,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group">
|
||||
<button type="submit" ng-click="login(username, password)"
|
||||
class="btn btn-primary btn-lg btn-block" translate>
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block" translate>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
@ -88,6 +88,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<button class="btn btn-default" ng-click="open()">
|
||||
<i class="fa fa-sign-in"></i>
|
||||
|
@ -43,6 +43,12 @@
|
||||
</div>
|
||||
|
||||
<div ng-controller="ProjectorCtrl">
|
||||
<style type="text/css">
|
||||
.scrollcontent {
|
||||
margin-top: {{scroll}}em;
|
||||
font-size: {{scale}}%;
|
||||
}
|
||||
</style>
|
||||
<div ng-repeat="element in elements">
|
||||
<div ng-include="element.template"></div>
|
||||
</div>
|
||||
|
@ -247,7 +247,7 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
|
||||
for key, value in request.data.items():
|
||||
if key not in projector_config:
|
||||
raise ValidationError({'data': 'Invalid projector element. Wrong UUID.'})
|
||||
projector_config.update(request.data)
|
||||
projector_config[key].update(request.data[key])
|
||||
|
||||
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
@ -183,6 +183,17 @@ class Speak(TestCase):
|
||||
|
||||
def test_begin_speech_with_countdown(self):
|
||||
config['agenda_couple_countdown_and_speakers'] = True
|
||||
projector = Projector.objects.get(pk=1)
|
||||
projector.config['03e87dea9c3f43c88b756c06a4c044fb'] = {
|
||||
'name': 'core/countdown',
|
||||
'status': 'stop',
|
||||
'visible': True,
|
||||
'default': 60,
|
||||
'countdown_time': 60,
|
||||
'stable': True,
|
||||
'index': 0
|
||||
}
|
||||
projector.save()
|
||||
Speaker.objects.add(self.user, self.item)
|
||||
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
|
||||
self.client.put(
|
||||
@ -199,6 +210,17 @@ class Speak(TestCase):
|
||||
|
||||
def test_end_speech_with_countdown(self):
|
||||
config['agenda_couple_countdown_and_speakers'] = True
|
||||
projector = Projector.objects.get(pk=1)
|
||||
projector.config['03e87dea9c3f43c88b756c06a4c044fb'] = {
|
||||
'name': 'core/countdown',
|
||||
'status': 'stop',
|
||||
'visible': True,
|
||||
'default': 60,
|
||||
'countdown_time': 60,
|
||||
'stable': True,
|
||||
'index': 0
|
||||
}
|
||||
projector.save()
|
||||
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
|
||||
speaker.begin_speech()
|
||||
self.client.delete(reverse('item-speak', args=[self.item.pk]))
|
||||
|
@ -33,6 +33,7 @@ class ProjectorAPI(TestCase):
|
||||
'elements': {
|
||||
'aae4a07b26534cfb9af4232f361dce73':
|
||||
{'id': customslide.id,
|
||||
'uuid': 'aae4a07b26534cfb9af4232f361dce73',
|
||||
'name': 'core/customslide',
|
||||
'context': None}},
|
||||
'scale': 0,
|
||||
@ -53,6 +54,7 @@ class ProjectorAPI(TestCase):
|
||||
'elements': {
|
||||
'fc6ef43b624043068c8e6e7a86c5a1b0':
|
||||
{'name': 'invalid_slide',
|
||||
'uuid': 'fc6ef43b624043068c8e6e7a86c5a1b0',
|
||||
'error': 'Projector element does not exist.'}},
|
||||
'scale': 0,
|
||||
'scroll': 0})
|
||||
|
Loading…
Reference in New Issue
Block a user