Merge pull request #2379 from FinnStutzenstein/Multiprojector

Multiprojector
This commit is contained in:
Norman Jäckel 2016-09-30 21:26:43 +02:00 committed by GitHub
commit 2a5bd6d94b
51 changed files with 2304 additions and 864 deletions

View File

@ -20,6 +20,7 @@ Core:
- Added support for big assemblies with lots of users. - Added support for big assemblies with lots of users.
- Added HTML support for messages on the projector. - Added HTML support for messages on the projector.
- Moved custom slides to own app "topics". Renamed it to "Topic". - Moved custom slides to own app "topics". Renamed it to "Topic".
- Added support for multiple projectors.
Motions: Motions:
- Added origin field. - Added origin field.

View File

@ -64,3 +64,32 @@ class ListOfSpeakersSlide(ProjectorElement):
# Full update if item changes because then we may have new speakers # Full update if item changes because then we may have new speakers
# and therefor need new users. # and therefor need new users.
return collection_element.collection_string == Item.get_collection_string() return collection_element.collection_string == Item.get_collection_string()
class CurrentListOfSpeakersSlide(ProjectorElement):
"""
Slide for the current list of speakers.
Nothing special to check.
"""
name = 'agenda/current-list-of-speakers'
def get_requirements(self, config_entry):
pk = config['projector_currentListOfSpeakers_reference']
if pk is not None:
# List of speakers slide.
try:
item = Item.objects.get(pk=pk)
except Item.DoesNotExist:
# Item does not exist. Just do nothing.
pass
else:
yield item
for speaker in item.speakers.filter(end_time=None):
# Yield current speaker and next speakers
yield speaker.user
query = (item.speakers.exclude(end_time=None)
.order_by('-end_time')[:config['agenda_show_last_speakers']])
for speaker in query:
# Yield last speakers
yield speaker.user

View File

@ -112,61 +112,84 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
} }
}, },
// override project function of jsDataModel factory // override project function of jsDataModel factory
project: function() { project: function (projectorId, tree) {
var isProjectedId = this.isProjected(tree);
if (isProjectedId > 0) {
// Deactivate
$http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
// Activate, if the projector_id is a new projector.
if (isProjectedId != projectorId) {
var name = tree ? 'agenda/item-list' : this.content_object.collection;
var id = tree ? this.id : this.content_object.id;
return $http.post( return $http.post(
'/rest/core/projector/1/prune_elements/', '/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: this.content_object.collection, id: this.content_object.id}] [{name: name, tree: tree, id: id}]
); );
}
}, },
// override isProjected function of jsDataModel factory // override isProjected function of jsDataModel factory
isProjected: function (list) { isProjected: function (tree) {
// Returns true if there is a projector element with the same // Returns the id of the last projector with an agenda-item element. Else return 0.
// name and the same id. if (typeof tree === 'undefined') {
var projector = Projector.get(1); tree = false;
var isProjected; }
if (typeof projector !== 'undefined') {
var self = this; var self = this;
var predicate = function (element) { var predicate = function (element) {
var value; var value;
if (typeof list === 'undefined') { if (tree) {
// Item tree slide for sub tree
value = element.name == 'agenda/item-list' &&
typeof element.id !== 'undefined' &&
element.id == self.id;
} else {
// Releated item detail slide // Releated item detail slide
value = element.name == self.content_object.collection && value = element.name == self.content_object.collection &&
typeof element.id !== 'undefined' && typeof element.id !== 'undefined' &&
element.id == self.content_object.id; element.id == self.content_object.id;
} else {
// Item list slide for sub tree
value = element.name == 'agenda/item-list' &&
typeof element.id !== 'undefined' &&
element.id == self.id;
} }
return value; return value;
}; };
isProjected = typeof _.findKey(projector.elements, predicate) === 'string'; var isProjected = 0;
} else { Projector.getAll().forEach(function (projector) {
isProjected = false; if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjected = projector.id;
} }
});
return isProjected; return isProjected;
}, },
// project list of speakers // project list of speakers
projectListOfSpeakers: function() { projectListOfSpeakers: function(projectorId) {
var isProjectedId = this.isListOfSpeakersProjected();
if (isProjectedId > 0) {
// Deactivate
$http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
// Activate
if (isProjectedId != projectorId) {
return $http.post( return $http.post(
'/rest/core/projector/1/prune_elements/', '/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: 'agenda/list-of-speakers', id: this.id}] [{name: 'agenda/list-of-speakers', id: this.id}]
); );
}
}, },
// check if list of speakers is projected // check if list of speakers is projected
isListOfSpeakersProjected: function () { isListOfSpeakersProjected: function () {
// Returns true if there is a projector element with the // Returns the id of the last projector with an element with the
// name 'agenda/list-of-speakers' and the same id. // name 'agenda/list-of-speakers' and the same id.
var projector = Projector.get(1);
if (typeof projector === 'undefined') return false;
var self = this; var self = this;
var predicate = function (element) { var predicate = function (element) {
return element.name == 'agenda/list-of-speakers' && return element.name == 'agenda/list-of-speakers' &&
typeof element.id !== 'undefined' && typeof element.id !== 'undefined' &&
element.id == self.id; element.id == self.id;
}; };
return typeof _.findKey(projector.elements, predicate) === 'string'; var isProjected = 0;
Projector.getAll().forEach(function (projector) {
if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjected = projector.id;
}
});
return isProjected;
}, },
hasSubitems: function(items) { hasSubitems: function(items) {
var self = this; var self = this;
@ -265,6 +288,56 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
} }
]) ])
// TODO: Remove all find() calls from the projector logic. It is also used on the site so this has to be
// changed with the refactoring of the site autoupdate.
.factory('CurrentListOfSpeakersItem', [
'Projector',
'Assignment', // TODO: Remove this after refactoring of data loading on start.
'Topic', // TODO: Remove this after refactoring of data loading on start.
'Motion', // TODO: Remove this after refactoring of data loading on start.
'Agenda',
function (Projector, Assignment, Topic, Motion, Agenda) {
return {
getItem: function (projectorId) {
var elementPromise;
return Projector.find(projectorId).then(function (projector) {
// scan all elements
_.forEach(projector.elements, function(element) {
switch(element.name) {
case 'motions/motion':
elementPromise = Motion.find(element.id).then(function(motion) {
return Motion.loadRelations(motion, 'agenda_item').then(function() {
return motion.agenda_item;
});
});
break;
case 'topics/topic':
elementPromise = Topic.find(element.id).then(function(topic) {
return Topic.loadRelations(topic, 'agenda_item').then(function() {
return topic.agenda_item;
});
});
break;
case 'assignments/assignment':
elementPromise = Assignment.find(element.id).then(function(assignment) {
return Assignment.loadRelations(assignment, 'agenda_item').then(function() {
return assignment.agenda_item;
});
});
break;
case 'agenda/list-of-speakers':
elementPromise = Agenda.find(element.id).then(function(item) {
return item;
});
}
});
return elementPromise;
});
}
};
}
])
// Make sure that the Agenda resource is loaded. // Make sure that the Agenda resource is loaded.
.run(['Agenda', function(Agenda) {}]); .run(['Agenda', function(Agenda) {}]);

View File

@ -13,6 +13,39 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
slidesProvider.registerSlide('agenda/item-list', { slidesProvider.registerSlide('agenda/item-list', {
template: 'static/templates/agenda/slide-item-list.html', template: 'static/templates/agenda/slide-item-list.html',
}); });
slidesProvider.registerSlide('agenda/current-list-of-speakers', {
template: 'static/templates/agenda/slide-current-list-of-speakers.html',
});
}
])
.controller('SlideCurrentListOfSpeakersCtrl', [
'$scope',
'Agenda',
'CurrentListOfSpeakersItem',
'Config',
function ($scope, Agenda, CurrentListOfSpeakersItem, Config) {
// Watch for changes in the current list of speakers reference
$scope.$watch(function () {
return Config.lastModified('projector_currentListOfSpeakers_reference');
}, function () {
$scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference');
$scope.updateCurrentListOfSpeakers();
});
// Watch for changes in the current item.
$scope.$watch(function () {
return Agenda.lastModified();
}, function () {
$scope.updateCurrentListOfSpeakers();
});
$scope.updateCurrentListOfSpeakers = function () {
var itemPromise = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference);
if (itemPromise) {
itemPromise.then(function(item) {
$scope.agendaItem = item;
});
}
};
} }
]) ])
@ -20,7 +53,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
'$scope', '$scope',
'Agenda', 'Agenda',
'User', 'User',
function($scope, Agenda, User) { function ($scope, Agenda, User) {
// Attention! Each object that is used here has to be dealt on server side. // 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 // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
@ -35,7 +68,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
'$filter', '$filter',
'Agenda', 'Agenda',
'AgendaTree', 'AgendaTree',
function($scope, $http, $filter, Agenda, AgendaTree) { function ($scope, $http, $filter, Agenda, AgendaTree) {
// Attention! Each object that is used here has to be dealt on server side. // 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 // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.

View File

@ -99,7 +99,8 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'TopicForm', // TODO: Remove this dependency. Use template hook for "New" and "Import" buttons. 'TopicForm', // TODO: Remove this dependency. Use template hook for "New" and "Import" buttons.
'AgendaTree', 'AgendaTree',
'Projector', 'Projector',
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector) { 'ProjectionDefault',
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector, ProjectionDefault) {
// Bind agenda tree to the scope // Bind agenda tree to the scope
$scope.$watch(function () { $scope.$watch(function () {
return Agenda.lastModified(); return Agenda.lastModified();
@ -110,6 +111,17 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
$scope.agendaHasSubitems = true; $scope.agendaHasSubitems = true;
} }
}); });
Projector.bindAll({}, $scope, 'projectors');
$scope.mainListTree = true;
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'agenda_all_items'})[0];
if (projectiondefault) {
$scope.defaultProjectorId_all_items = projectiondefault.projector_id;
}
$scope.projectionDefaults = ProjectionDefault.getAll();
});
$scope.alert = {}; $scope.alert = {};
$scope.sumDurations = function () { $scope.sumDurations = function () {
@ -218,34 +230,78 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
$scope.uncheckAll(); $scope.uncheckAll();
}; };
/** Project functions **/
// get ProjectionDefault for item
$scope.getProjectionDefault = function (item) {
if (item.tree) {
return $scope.defaultProjectorId_all_items;
} else {
var app_name = item.content_object.collection.split('/')[0];
var id = 1;
$scope.projectionDefaults.forEach(function (projectionDefault) {
if (projectionDefault.name == app_name) {
id = projectionDefault.projector_id;
}
});
return id;
}
};
// project agenda // project agenda
$scope.projectAgenda = function (tree, id) { $scope.projectAgenda = function (projectorId, tree, id) {
$http.post('/rest/core/projector/1/prune_elements/', var isAgendaProjectedId = $scope.isAgendaProjected($scope.mainListTree);
if (isAgendaProjectedId > 0) {
// Deactivate
$http.post('/rest/core/projector/' + isAgendaProjectedId + '/clear_elements/');
}
if (isAgendaProjectedId != projectorId) {
$http.post('/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: 'agenda/item-list', tree: tree, id: id}]); [{name: 'agenda/item-list', tree: tree, id: id}]);
}
};
// change whether all items or only main items should be projected
$scope.changeMainListTree = function () {
var isAgendaProjectedId = $scope.isAgendaProjected($scope.mainListTree);
$scope.mainListTree = !$scope.mainListTree;
if (isAgendaProjectedId > 0) {
$scope.projectAgenda(isAgendaProjectedId, $scope.mainListTree);
}
};
// change whether one item or all subitems should be projected
$scope.changeItemTree = function (item) {
var isProjected = item.isProjected(item.tree);
if (isProjected > 0) {
// Deactivate and reactivate
item.project(isProjected, item.tree);
item.project(isProjected, !item.tree);
}
item.tree = !item.tree;
}; };
// check if agenda is projected // check if agenda is projected
$scope.isAgendaProjected = function (tree) { $scope.isAgendaProjected = function (tree) {
// Returns true if there is a projector element with the name // Returns true if there is a projector element with the name
// 'agenda/item-list'. // 'agenda/item-list'.
var projector = Projector.get(1);
if (typeof projector === 'undefined') return false;
var self = this;
var predicate = function (element) { var predicate = function (element) {
var value; var value;
if (typeof tree === 'undefined') { if (tree) {
// only main agenda items
value = element.name == 'agenda/item-list' &&
typeof element.id === 'undefined' &&
!element.tree;
} else {
// tree with all agenda items // tree with all agenda items
value = element.name == 'agenda/item-list' && value = element.name == 'agenda/item-list' &&
typeof element.id === 'undefined' && typeof element.id === 'undefined' &&
element.tree; element.tree;
} else {
// only main agenda items
value = element.name == 'agenda/item-list' &&
typeof element.id === 'undefined' &&
!element.tree;
} }
return value; return value;
}; };
return typeof _.findKey(projector.elements, predicate) === 'string'; var projectorId = 0;
$scope.projectors.forEach(function (projector) {
if (typeof _.findKey(projector.elements, predicate) === 'string') {
projectorId = projector.id;
}
});
return projectorId;
}; };
// auto numbering of agenda items // auto numbering of agenda items
$scope.autoNumbering = function() { $scope.autoNumbering = function() {
@ -263,9 +319,24 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'Agenda', 'Agenda',
'User', 'User',
'item', 'item',
function ($scope, $filter, $http, $state, operator, Agenda, User, item) { 'Projector',
'ProjectionDefault',
function ($scope, $filter, $http, $state, operator, Agenda, User, item, Projector, ProjectionDefault) {
Agenda.bindOne(item.id, $scope, 'item'); Agenda.bindOne(item.id, $scope, 'item');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var item_app_name = item.content_object.collection.split('/')[0];
var projectiondefaultItem = ProjectionDefault.filter({name: item_app_name})[0];
if (projectiondefaultItem) {
$scope.defaultProjectorItemId = projectiondefaultItem.projector_id;
}
var projectiondefaultListOfSpeakers = ProjectionDefault.filter({name: 'agenda_list_of_speakers'})[0];
if (projectiondefaultListOfSpeakers) {
$scope.defaultProjectorListOfSpeakersId = projectiondefaultListOfSpeakers.projector_id;
}
});
$scope.speakerSelectBox = {}; $scope.speakerSelectBox = {};
$scope.alert = {}; $scope.alert = {};
$scope.speakers = $filter('orderBy')(item.speakers, 'weight'); $scope.speakers = $filter('orderBy')(item.speakers, 'weight');
@ -428,50 +499,64 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'$state', '$state',
'$http', '$http',
'Projector', 'Projector',
'Assignment', // TODO: Remove this after refactoring of data loading on start. 'ProjectionDefault',
'Topic', // TODO: Remove this after refactoring of data loading on start. 'Config',
'Motion', // TODO: Remove this after refactoring of data loading on start. 'CurrentListOfSpeakersItem',
'Agenda', function($scope, $state, $http, Projector, ProjectionDefault, Config, CurrentListOfSpeakersItem) {
function($scope, $state, $http, Projector, Assignment, Topic, Motion, Agenda) { // Watch for changes in the current list of speakers reference
$scope.$watch( $scope.$watch(function () {
function() { return Config.lastModified('projector_currentListOfSpeakers_reference');
return Projector.lastModified(1); }, function () {
}, $scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference');
function() { $scope.updateCurrentListOfSpeakers();
Projector.find(1).then( function(projector) {
$scope.AgendaItem = null;
_.forEach(projector.elements, function(element) {
switch(element.name) {
case 'motions/motion':
Motion.find(element.id).then(function(motion) {
Motion.loadRelations(motion, 'agenda_item').then(function() {
$scope.AgendaItem = motion.agenda_item;
}); });
$scope.$watch(function() {
return Projector.lastModified();
}, function() {
$scope.projectors = Projector.getAll();
$scope.updateCurrentListOfSpeakers();
var projectiondefault = ProjectionDefault.filter({name: 'agenda_current_list_of_speakers'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
}); });
break; $scope.updateCurrentListOfSpeakers = function () {
case 'topics/topic': var itemPromise = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference);
Topic.find(element.id).then(function(topic) { if (itemPromise) {
Topic.loadRelations(topic, 'agenda_item').then(function() { itemPromise.then(function(item) {
$scope.AgendaItem = topic.agenda_item;
});
});
break;
case 'assignments/assignment':
Assignment.find(element.id).then(function(assignment) {
Assignment.loadRelations(assignment, 'agenda_item').then(function() {
$scope.AgendaItem = assignment.agenda_item;
});
});
break;
case 'agenda/list-of-speakers':
Agenda.find(element.id).then(function(item) {
$scope.AgendaItem = item; $scope.AgendaItem = item;
}); });
} }
}); };
}); // Project current list of speakers
// same logic as in core/base.js
$scope.projectCurrentLoS = function (projectorId) {
var isCurrentLoSProjectedId = $scope.isCurrentLoSProjected($scope.mainListTree);
if (isCurrentLoSProjectedId > 0) {
// Deactivate
$http.post('/rest/core/projector/' + isCurrentLoSProjectedId + '/clear_elements/');
} }
); if (isCurrentLoSProjectedId != projectorId) {
$http.post('/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: 'agenda/current-list-of-speakers'}]);
}
};
// same logic as in core/base.js
$scope.isCurrentLoSProjected = function () {
// Returns the projector id if there is a projector element with the name
// 'agenda/current-list-of-speakers'. Elsewise returns 0.
var projectorId = 0;
$scope.projectors.forEach(function (projector) {
var key = _.findKey(projector.elements, function (element) {
return element.name == 'agenda/current-list-of-speakers';
});
if (typeof key === 'string') {
projectorId = projector.id;
}
});
return projectorId;
};
// go to the list of speakers (management) of the currently // go to the list of speakers (management) of the currently
// displayed projector slide // displayed projector slide
$scope.goToListOfSpeakers = function() { $scope.goToListOfSpeakers = function() {

View File

@ -1,11 +1,32 @@
<div class="header"> <div class="header">
<div class="title"> <div class="title">
<div class="submenu"> <div class="submenu">
<button ng-click="isFullScreen = !isFullScreen" <div class="form-inline">
class="btn btn-sm btn-default"> <div os-perms="core.can_manage_projector" class="btn-group" uib-dropdown
<i class="fa fa-expand fa-lg"></i> uib-tooltip="{{ 'Projector' | translate }} {{ isCurrentLoSProjected() }}"
<translate>Fullscreen</translate> tooltip-enable="isCurrentLoSProjected() > 0">
<button type="button" class="btn btn-default btn-sm"
title="{{ 'Project current list of speakers' | translate }}"
ng-click="projectCurrentLoS(defaultProjectorId)"
ng-class="{ 'btn-primary': isCurrentLoSProjected() > 0 && isCurrentLoSProjected() == defaultProjectorId}">
<i class="fa fa-video-camera"></i>
<translate>Current list of speakers</translate>
</button> </button>
<button type="button" class="btn btn-default btn-sm" uib-dropdown-toggle
ng-class="{ 'btn-primary': isCurrentLoSProjected() > 0 && isCurrentLoSProjected() != defaultProjectorId}">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button">
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="projectCurrentLoS(projector.id)"
ng-class="{ 'projected': isCurrentLoSProjected() == projector.id }">
<i class="fa fa-video-camera" ng-show="isCurrentLoSProjected() == projector.id"></i>
{{ projector.name }}
<span ng-if="projector.id == defaultProjectorId">(<translate>Standard</translate>)</span>
</a>
</li>
</ul>
</div>
<button os-perms="agenda.can_manage" <button os-perms="agenda.can_manage"
ng-click="goToListOfSpeakers()" ng-click="goToListOfSpeakers()"
class="btn btn-sm btn-default"> class="btn btn-sm btn-default">
@ -13,24 +34,17 @@
<translate>Manage list</translate> <translate>Manage list</translate>
</button> </button>
</div> </div>
<h1 translate>List of speakers</h1> </div>
<h1 translate>Current list of speakers</h1>
<h2> {{ AgendaItem.getTitle() }} <h2> {{ AgendaItem.getTitle() }}
<span class="slimlabel label label-danger ng-scope" style="" ng-if="AgendaItem.speaker_list_closed" translate> <span class="slimlabel label label-danger ng-scope" style="" ng-if="AgendaItem.speaker_list_closed" translate>
Closed Closed
</span> </span>
</h2> </h2>
</div>
</div> </div>
<div class="content" ng-class="isFullScreen ? 'fullscreendiv' : 'details'" <div class="details">
ng-click="isFullScreen? (isFullScreen = !isFullScreen) : a">
<div ng-if="isFullScreen" class="fullscreendiv-title">
<h1 translate>List of speakers</h1>
<h2> {{ AgendaItem.getTitle() }}
<span class="slimlabel label label-danger ng-scope" style=""
ng-if="AgendaItem.speaker_list_closed" translate>Closed</span>
</h2>
</div>
<div class="content">
<!-- Last speakers --> <!-- Last speakers -->
<p ng-repeat="speaker in lastSpeakers = (AgendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) | <p ng-repeat="speaker in lastSpeakers = (AgendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers"> limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers">
@ -44,5 +58,4 @@
<li ng-repeat="speaker in AgendaItem.speakers | filter: {begin_time: null} | orderBy:'weight'"> <li ng-repeat="speaker in AgendaItem.speakers | filter: {begin_time: null} | orderBy:'weight'">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
</ol> </ol>
</div>
</div> </div>

View File

@ -10,20 +10,37 @@
{{ item.getContentResource().verboseName | translate }} {{ item.getContentResource().verboseName | translate }}
</a> </a>
<!-- project list of speakers --> <!-- project list of speakers -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <span class="btn-group" style="min-width:54px;" uib-dropdown
ng-class="{ 'btn-primary': item.isListOfSpeakersProjected() }" uib-tooltip="{{ 'Projektor' | translate }} {{ item.isListOfSpeakersProjected() }}"
ng-click="item.projectListOfSpeakers()"> tooltip-enable="item.isListofSpeakersProjected() > 0"
os-perms="core.can_manage_projector">
<button type="button" class="btn btn-default btn-sm"
ng-click="item.projectListOfSpeakers(defaultProjectorListOfSpeakersId)"
ng-class="{ 'btn-primary': item.isListOfSpeakersProjected() == defaultProjectorListOfSpeakersId }">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
<translate>List of speakers</translate> <translate>List of speakers</translate>
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-class="{ 'btn-primary': (item.isListOfSpeakersProjected() > 0 && item.isListOfSpeakersProjected() != defaultProjectorListOfSpeakersId) }"
ng-if="projectors.length > 1"
uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="item.projectListOfSpeakers(projector.id)"
ng-class="{ 'projected': (item.isListOfSpeakersProjected() == projector.id) }">
<i class="fa fa-video-camera" ng-show="item.isListOfSpeakersProjected() == projector.id"></i>
{{ projector.name }}
<span ng-if="defaultProjectorListOfSpeakersId == projector.id">(<translate>Standard</translate>)</span>
</a> </a>
</li>
</ul>
</span>
<!-- project --> <!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <projector-button model="item" default-projector-id="defaultProjectorItemId"
ng-class="{ 'btn-primary': item.isProjected() }" content="{{ item.getContentResource().verboseName | translate }}">
ng-click="item.project()" </projector-button>
title="{{ 'Project item' | translate }}">
<i class="fa fa-video-camera"></i>
{{ item.getContentResource().verboseName | translate }}
</a>
</div> </div>
<h1>{{ item.getTitle() }}</h1> <h1>{{ item.getTitle() }}</h1>
<h2> <h2>

View File

@ -38,26 +38,37 @@
<translate>Select ...</translate> <translate>Select ...</translate>
</button> </button>
<!-- project agenda button --> <!-- project agenda button -->
<div os-perms="core.can_manage_projector" class="btn-group" uib-dropdown> <div class="btn-group" uib-dropdown
<button uib-tooltip="{{ 'Projector' | translate }} {{ isAgendaProjected(mainListTree) }}"
id="project-agenda-button" tooltip-enable="isAgendaProjected(mainListTree) > 0"
type="button" os-perms="core.can_manage_projector">
class="btn btn-default" <button type="button" class="btn btn-default"
title="{{ 'Project agenda' | translate }}" title="{{ 'Project agenda' | translate }}"
ng-click="projectAgenda(tree=true)" ng-click="projectAgenda(defaultProjectorId_all_items, mainListTree)"
ng-class="{ 'btn-primary': isAgendaProjected(tree=true) }"> ng-class="{ 'btn-primary': isAgendaProjected(mainListTree) > 0 && isAgendaProjected(mainListTree) == defaultProjectorId_all_items}">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
<translate>Agenda</translate> <translate>Agenda</translate>
</button> </button>
<button type="button" class="btn btn-default" <button type="button" class="btn btn-default" uib-dropdown-toggle
ng-if="agendaHasSubitems" ng-class="{ 'btn-primary': isAgendaProjected(mainListTree) > 0 && isAgendaProjected(mainListTree) != defaultProjectorId_all_items}">
ng-class="{ 'btn-primary': isAgendaProjected() }"
uib-dropdown-toggle>
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="project-agenda-button"> <ul class="dropdown-menu" role="menu" aria-labelledby="split-button">
<li role="menuitem"><a href="" ng-click="projectAgenda(tree=true)" translate>All agenda items (Default)</a> <li role="menuitem" ng-show="agendaHasSubitems">
<li role="menuitem"><a href="" ng-click="projectAgenda(tree=false)" translate>Only main agenda items</a> <a href="" ng-click="changeMainListTree(); $event.stopPropagation();">
<i class="fa" ng-class="mainListTree ? 'fa-square-o' : 'fa-check-square-o'"></i>
<translate>Only main agenda items</translate>
</a>
</li>
<li class="divider" ng-show="agendaHasSubitems"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="projectAgenda(projectorId=projector.id, tree=mainListTree)"
ng-class="{ 'projected': isAgendaProjected(mainListTree) == projector.id }">
<i class="fa fa-video-camera" ng-show="isAgendaProjected(mainListTree) == projector.id"></i>
{{ projector.name }}
<span ng-if="projector.id == defaultProjectorId_all_items">(<translate>Standard</translate>)</span>
</a>
</li>
</ul> </ul>
</div> </div>
<!-- auto numbering button --> <!-- auto numbering button -->
@ -71,7 +82,7 @@
<a os-perms="users.can_see_name" class="btn btn-default" <a os-perms="users.can_see_name" class="btn btn-default"
ui-sref="agenda.current-list-of-speakers"> ui-sref="agenda.current-list-of-speakers">
<i class="fa fa-microphone"></i> <i class="fa fa-microphone"></i>
<translate>List of speakers</translate> <translate>Current list of speakers</translate>
</a> </a>
</div> </div>
</div> </div>
@ -150,25 +161,37 @@
ng-class="{ 'activeline': item.isProjected(), 'selected': item.selected, 'hiddenrow': item.is_hidden}"> ng-class="{ 'activeline': item.isProjected(), 'selected': item.selected, 'hiddenrow': item.is_hidden}">
<!-- projector column --> <!-- projector column -->
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector"> <td ng-show="!isDeleteMode" os-perms="core.can_manage_projector">
<div class="btn-group" style="width:54px;" uib-dropdown> <div class="btn-group" style="min-width:54px;" uib-dropdown
<button os-perms="core.can_manage_projector" uib-tooltip="{{ 'Projector' | translate }} {{ item.isProjected(item.tree) }}"
id="project-item" tooltip-enable="item.isProjected(item.tree) > 0">
type="button" <button class="btn btn-default btn-sm"
class="btn btn-default btn-sm"
title="{{ 'Project item' | translate }}" title="{{ 'Project item' | translate }}"
ng-click="item.project()" ng-click="item.project(getProjectionDefault(item), item.tree)"
ng-class="{ 'btn-primary': item.isProjected() }"> ng-class="{ 'btn-primary': item.isProjected(item.tree) > 0 && item.isProjected(item.tree) == getProjectionDefault(item)}">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</button> </button>
<button type="button" class="btn btn-default btn-sm slimDropDown" <button type="button" class="btn btn-default btn-sm slimDropDown"
ng-if="item.hasSubitems(items)" ng-class="{ 'btn-primary': item.isProjected(item.tree) > 0 && item.isProjected(item.tree) != getProjectionDefault(item)}"
ng-class="{ 'btn-primary': item.isProjected(list=true) }" ng-show="item.hasSubitems(items) || projectors.length > 1"
uib-dropdown-toggle> uib-dropdown-toggle>
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="project-item"> <ul class="dropdown-menu" role="menu" aria-labelledby="split-button">
<li role="menuitem"><a href="" ng-click="item.project()" translate>Project item (Default)</a> <li role="menuitem" ng-show="item.hasSubitems(items)">
<li role="menuitem"><a href="" ng-click="projectAgenda(tree=true, id=item.id)" translate>Project all sub items</a> <a href="" ng-click="changeItemTree(item); $event.stopPropagation();">
<i class="fa" ng-class="item.tree ? 'fa-check-square-o' : 'fa-square-o'"></i>
<translate>Include all sub items</translate>
</a>
</li>
<li class="divider" ng-show="item.hasSubitems(items)"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="item.project(projector.id, item.tree)"
ng-class="{ 'projected': item.isProjected(item.tree) == projector.id }">
<i class="fa fa-video-camera" ng-show="item.isProjected(item.tree) == projector.id"></i>
{{ projector.name }}
<span ng-if="projector.id == getProjectionDefault(item)">(<translate>Standard</translate>)</span>
</a>
</li>
</ul> </ul>
</div> </div>
<!-- delete selection column --> <!-- delete selection column -->

View File

@ -0,0 +1,23 @@
<div ng-controller="SlideCurrentListOfSpeakersCtrl" class="content scrollcontent">
<div class="title">
<h1 translate>Current List of speakers</h1>
<h2> {{ agendaItem.getTitle() }}
<span class="slimlabel label label-danger ng-scope" style=""
ng-if="agendaItem.speaker_list_closed" translate>Closed</span>
</h2>
</div>
<!-- Last speakers -->
<p ng-repeat="speaker in lastSpeakers = (agendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers">
{{ speaker.user.get_full_name() }}
<!-- Current speaker -->
<p ng-repeat="speaker in currentspeakers = (agendaItem.speakers| filter: {end_time: null, begin_time: '!!'})"
class="currentSpeaker">
<i class="fa fa-microphone fa-lg"></i> {{ speaker.user.get_full_name() }}
<!-- Next speakers -->
<ol class="nextSpeakers">
<li ng-repeat="speaker in agendaItem.speakers | filter: {begin_time: null} | orderBy:'weight'">
{{ speaker.user.get_full_name() }}
</ol>
</div>

View File

@ -267,19 +267,22 @@ angular.module('OpenSlidesApp.assignments', [])
return "Election"; return "Election";
}, },
// override project function of jsDataModel factory // override project function of jsDataModel factory
project: function (poll_id) { project: function (projectorId, pollId) {
var isProjectedId = this.isProjected(pollId);
if (isProjectedId > 0) {
$http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
if (isProjectedId != projectorId) {
return $http.post( return $http.post(
'/rest/core/projector/1/prune_elements/', '/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: 'assignments/assignment', id: this.id, poll: poll_id}] [{name: 'assignments/assignment', id: this.id, poll: pollId}]
); );
}
}, },
// override isProjected function of jsDataModel factory // override isProjected function of jsDataModel factory
isProjected: function (poll_id) { isProjected: function (poll_id) {
// Returns true if there is a projector element with the name // Returns the id of the last projector found with an element
// 'assignments/assignment'. // with the name 'assignments/assignment'.
var projector = Projector.get(1);
var isProjected;
if (typeof projector !== 'undefined') {
var self = this; var self = this;
var predicate = function (element) { var predicate = function (element) {
var value; var value;
@ -299,10 +302,12 @@ angular.module('OpenSlidesApp.assignments', [])
} }
return value; return value;
}; };
isProjected = typeof _.findKey(projector.elements, predicate) === 'string'; var isProjected = 0;
} else { Projector.getAll().forEach(function (projector) {
isProjected = false; if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjected = projector.id;
} }
});
return isProjected; return isProjected;
} }
}, },

View File

@ -233,9 +233,19 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
'Tag', 'Tag',
'Agenda', 'Agenda',
'phases', 'phases',
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, phases) { 'Projector',
'ProjectionDefault',
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, phases, Projector, ProjectionDefault) {
Assignment.bindAll({}, $scope, 'assignments'); Assignment.bindAll({}, $scope, 'assignments');
Tag.bindAll({}, $scope, 'tags'); Tag.bindAll({}, $scope, 'tags');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.phases = phases; $scope.phases = phases;
$scope.alert = {}; $scope.alert = {};
@ -339,10 +349,21 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
'User', 'User',
'assignment', 'assignment',
'phases', 'phases',
function($scope, $http, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment, User, assignment, phases) { 'Projector',
'ProjectionDefault',
function($scope, $http, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment, User,
assignment, phases, Projector, ProjectionDefault) {
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Assignment.bindOne(assignment.id, $scope, 'assignment'); Assignment.bindOne(assignment.id, $scope, 'assignment');
Assignment.loadRelations(assignment, 'agenda_item'); Assignment.loadRelations(assignment, 'agenda_item');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.candidateSelectBox = {}; $scope.candidateSelectBox = {};
$scope.phases = phases; $scope.phases = phases;
$scope.alert = {}; $scope.alert = {};

View File

@ -16,12 +16,8 @@
<translate>List of speakers</translate> <translate>List of speakers</translate>
</a> </a>
<!-- project --> <!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <projector-button model="assignment", default-projector-id="defaultProjectorId">
ng-class="{ 'btn-primary': assignment.isProjected() }" </projector-button>
ng-click="assignment.project()"
title="{{ 'Project election' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<!-- edit --> <!-- edit -->
<a os-perms="assignments.can_manage" ng-click="openDialog(assignment)" <a os-perms="assignments.can_manage" ng-click="openDialog(assignment)"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
@ -155,13 +151,9 @@
3. <translate>Published</translate> 3. <translate>Published</translate>
</button> </button>
<i class="fa fa-arrow-right"></i> <i class="fa fa-arrow-right"></i>
<button os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <projector-button model="assignment" default-projector-id="defaultProjectorId"
ng-class="{ 'btn-primary': assignment.isProjected(poll.id) }" additional-id="poll.id" content="4. {{ 'Project' | translate }}">
ng-click="assignment.project(poll.id)" </projector-button>
title="{{ 'Project ballot' | translate }}">
<i class="fa fa-video-camera"></i>
4. <translate>Project</translate>
</button>
<a class="btn btn-danger btn-sm" <a class="btn btn-danger btn-sm"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this ballot?' | translate }}" ng-bootbox-confirm="{{ 'Are you sure you want to delete this ballot?' | translate }}"
ng-bootbox-confirm-action="deleteBallot(poll)"> ng-bootbox-confirm-action="deleteBallot(poll)">

View File

@ -121,12 +121,8 @@
<!-- projector --> <!-- projector -->
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector"> <td ng-show="!isDeleteMode" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm" <projector-button model="assignment" default-projector-id="defaultProjectorId">
ng-class="{ 'btn-primary': assignment.isProjected() }" </projector-button>
ng-click="assignment.project()"
title="{{ 'Project election' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<!-- delete selection --> <!-- delete selection -->
<td ng-show="isDeleteMode" os-perms="assignments.can_manage" class="deleteColumn"> <td ng-show="isDeleteMode" os-perms="assignments.can_manage" class="deleteColumn">

View File

@ -20,7 +20,7 @@ class CoreAppConfig(AppConfig):
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from openslides.utils.search import index_add_instance, index_del_instance from openslides.utils.search import index_add_instance, index_del_instance
from .config_variables import get_config_variables from .config_variables import get_config_variables
from .signals import delete_django_app_permissions from .signals import delete_django_app_permissions, create_builtin_projection_defaults
from .views import ( from .views import (
ChatMessageViewSet, ChatMessageViewSet,
ConfigViewSet, ConfigViewSet,
@ -35,6 +35,9 @@ class CoreAppConfig(AppConfig):
post_permission_creation.connect( post_permission_creation.connect(
delete_django_app_permissions, delete_django_app_permissions,
dispatch_uid='delete_django_app_permissions') dispatch_uid='delete_django_app_permissions')
post_permission_creation.connect(
create_builtin_projection_defaults,
dispatch_uid='create_builtin_projection_defaults')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)

View File

@ -4,16 +4,14 @@ from django.utils.translation import ugettext as _
from .exceptions import ConfigError, ConfigNotFound from .exceptions import ConfigError, ConfigNotFound
from .models import ConfigStore from .models import ConfigStore
# remove resolution when changing to multiprojector
INPUT_TYPE_MAPPING = { INPUT_TYPE_MAPPING = {
'string': str, 'string': str,
'text': str, 'text': str,
'integer': int, 'integer': int,
'boolean': bool, 'boolean': bool,
'choice': str, 'choice': str,
'colorpicker': str,
'comments': list, 'comments': list,
'resolution': dict} 'colorpicker': str}
class ConfigHandler: class ConfigHandler:
@ -89,16 +87,6 @@ class ConfigHandler:
except DjangoValidationError as e: except DjangoValidationError as e:
raise ConfigError(e.messages[0]) raise ConfigError(e.messages[0])
# remove this block when changing to multiprojector
if config_variable.input_type == 'resolution':
if value.get('width') is None or value.get('height') is None:
raise ConfigError(_('A width and a height have to be given.'))
if not isinstance(value['width'], int) or not isinstance(value['height'], int):
raise ConfigError(_('Data has to be integers.'))
if (value['width'] < 800 or value['width'] > 3840 or
value['height'] < 600 or value['height'] > 2160):
raise ConfigError(_('The Resolution have to be between 800x600 and 3840x2160.'))
if config_variable.input_type == 'comments': if config_variable.input_type == 'comments':
if not isinstance(value, list): if not isinstance(value, list):
raise ConfigError(_('motions_comments has to be a list.')) raise ConfigError(_('motions_comments has to be a list.'))

View File

@ -157,11 +157,28 @@ def get_config_variables():
weight=185, weight=185,
group='Projector') group='Projector')
# set the resolution for one projector. It can be removed with the multiprojector feature.
yield ConfigVariable( yield ConfigVariable(
name='projector_resolution', name='projector_blank_color',
default_value={'width': 1024, 'height': 768}, default_value='#FFFFFF',
input_type='resolution', input_type='colorpicker',
label='Projector Resolution', label='Color for blanked projector',
weight=200, weight=190,
group='Projector') group='Projector')
yield ConfigVariable(
name='projector_broadcast',
default_value=0,
input_type='integer',
label='Projector which is broadcasted',
weight=200,
group='Projector',
hidden=True)
yield ConfigVariable(
name='projector_currentListOfSpeakers_reference',
default_value=1,
input_type='integer',
label='Projector reference for list of speakers',
weight=201,
group='Projector',
hidden=True)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.9 on 2016-08-29 09:37
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
import openslides.utils.models
def name_default_projector(apps, schema_editor):
"""
Set the name of the default projector to 'Defaultprojector'
"""
Projector = apps.get_model('core', 'Projector')
Projector.objects.filter(pk=1).update(name='Default projector')
class Migration(migrations.Migration):
dependencies = [
('core', '0005_auto_20160918_2104'),
]
operations = [
migrations.CreateModel(
name='ProjectionDefault',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256)),
('display_name', models.CharField(max_length=256)),
],
options={
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model),
),
migrations.AddField(
model_name='projector',
name='name',
field=models.CharField(blank=True, max_length=255, unique=True),
),
migrations.AddField(
model_name='projector',
name='blank',
field=models.BooleanField(blank=False, default=False),
),
migrations.AddField(
model_name='projectiondefault',
name='projector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projectiondefaults', to='core.Projector'),
),
migrations.RunPython(name_default_projector),
]

View File

@ -63,11 +63,19 @@ class Projector(RESTModelMixin, models.Model):
scroll = models.IntegerField(default=0) scroll = models.IntegerField(default=0)
# currently unused, but important for the multiprojector.
width = models.PositiveIntegerField(default=1024) width = models.PositiveIntegerField(default=1024)
height = models.PositiveIntegerField(default=768) height = models.PositiveIntegerField(default=768)
name = models.CharField(
max_length=255,
unique=True,
blank=True)
blank = models.BooleanField(
blank=False,
default=False)
class Meta: class Meta:
""" """
Contains general permissions that can not be placed in a specific app. Contains general permissions that can not be placed in a specific app.
@ -169,6 +177,34 @@ class Projector(RESTModelMixin, models.Model):
return result return result
class ProjectionDefault(RESTModelMixin, models.Model):
"""
Model for the projection defaults like motions, agenda, list of
speakers and thelike. The name is the technical name like 'topics' or
'motions'. For apps the name should be the app name to get keep the
ProjectionDefault for apps generic. But it is possible to give some
special name like 'list_of_speakers'. The display_name is the shown
name on the front end for the user.
"""
name = models.CharField(max_length=256)
display_name = models.CharField(max_length=256)
projector = models.ForeignKey(
Projector,
on_delete=models.CASCADE,
related_name='projectiondefaults')
def get_root_rest_element(self):
return self.projector
class Meta:
default_permissions = ()
def __str__(self):
return self.display_name
class Tag(RESTModelMixin, models.Model): class Tag(RESTModelMixin, models.Model):
""" """
Model for tags. This tags can be used for other models like agenda items, Model for tags. This tags can be used for other models like agenda items,

View File

@ -1,3 +1,5 @@
import uuid
from django.utils.timezone import now from django.utils.timezone import now
from ..utils.projector import ProjectorElement from ..utils.projector import ProjectorElement
@ -20,7 +22,7 @@ class Countdown(ProjectorElement):
To start the countdown write into the config field: To start the countdown write into the config field:
{ {
"status": "running", "running": True,
"countdown_time": <timestamp>, "countdown_time": <timestamp>,
} }
@ -30,10 +32,10 @@ class Countdown(ProjectorElement):
To stop the countdown set the countdown time to the current value of the To stop the countdown set the countdown time to the current value of the
countdown (countdown_time = countdown_time - now + serverTimeOffset) countdown (countdown_time = countdown_time - now + serverTimeOffset)
and set status to "stop". and set running to False.
To reset the countdown (it is not a reset in a functional way) just To reset the countdown (it is not a reset in a functional way) just
change the countdown time. The status value remains "stop". change the countdown time. The running value remains False.
Do not forget to send values for additional keywords like "stable" if Do not forget to send values for additional keywords like "stable" if
you do not want to use the default. you do not want to use the default.
@ -63,28 +65,56 @@ class Countdown(ProjectorElement):
""" """
if not isinstance(config_data.get('countdown_time'), (int, float)): if not isinstance(config_data.get('countdown_time'), (int, float)):
raise ProjectorException('Invalid countdown time. Use integer or float.') raise ProjectorException('Invalid countdown time. Use integer or float.')
if config_data.get('status') not in ('running', 'stop'): if not isinstance(config_data.get('running'), bool):
raise ProjectorException("Invalid status. Use 'running' or 'stop'.") raise ProjectorException("Invalid running status. Has to be a boolean.")
if config_data.get('default') is not None and not isinstance(config_data.get('default'), int): if config_data.get('default') is not None and not isinstance(config_data.get('default'), int):
raise ProjectorException('Invalid default value. Use integer.') raise ProjectorException('Invalid default value. Use integer.')
@classmethod @classmethod
def control(cls, action, projector_id=1, index=0): def control(cls, action):
"""
Starts, stops or resets the countdown with the given index on the
given projector.
Action must be 'start', 'stop' or 'reset'.
"""
if action not in ('start', 'stop', 'reset'): if action not in ('start', 'stop', 'reset'):
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action)) raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
projector_instance = Projector.objects.get(pk=projector_id) # Use the countdown with the lowest index
projectors = Projector.objects.all()
lowest_index = None
if projectors[0]:
for key, value in projectors[0].config.items():
if value['name'] == cls.name:
if lowest_index is None or value['index'] < lowest_index:
lowest_index = value['index']
if lowest_index is None:
# create a countdown
for projector in projectors:
projector_config = {}
for key, value in projector.config.items():
projector_config[key] = value
# new countdown
countdown = {
'name': 'core/countdown',
'stable': True,
'index': 1,
'default_time': config['projector_default_countdown'],
'visible': False,
'selected': True,
}
if action == 'start':
countdown['running'] = True
countdown['countdown_time'] = now().timestamp() + countdown['default_time']
elif action == 'reset' or action == 'stop':
countdown['running'] = False
countdown['countdown_time'] = countdown['default_time']
projector_config[uuid.uuid4().hex] = countdown
projector.config = projector_config
projector.save()
else:
# search for the countdown and modify it.
for projector in projectors:
projector_config = {} projector_config = {}
found = False found = False
for key, value in projector_instance.config.items(): for key, value in projector.config.items():
if value['name'] == cls.name: if value['name'] == cls.name and value['index'] == lowest_index:
if index == 0:
try: try:
cls.validate_config(value) cls.validate_config(value)
except ProjectorException: except ProjectorException:
@ -92,21 +122,19 @@ class Countdown(ProjectorElement):
# The variable found remains False. # The variable found remains False.
break break
found = True found = True
if action == 'start' and value['status'] == 'stop': if action == 'start':
value['status'] = 'running' value['running'] = True
value['countdown_time'] = now().timestamp() + value['countdown_time'] value['countdown_time'] = now().timestamp() + value['default_time']
elif action == 'stop' and value['status'] == 'running': elif action == 'stop' and value['running']:
value['status'] = 'stop' value['running'] = False
value['countdown_time'] = value['countdown_time'] - now().timestamp() value['countdown_time'] = value['countdown_time'] - now().timestamp()
elif action == 'reset': elif action == 'reset':
value['status'] = 'stop' value['running'] = False
value['countdown_time'] = value.get('default', config['projector_default_countdown']) value['countdown_time'] = value['default_time']
else:
index += -1
projector_config[key] = value projector_config[key] = value
if found: if found:
projector_instance.config = projector_config projector.config = projector_config
projector_instance.save() projector.save()
class Message(ProjectorElement): class Message(ProjectorElement):

View File

@ -1,6 +1,6 @@
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
from .models import ChatMessage, Projector, Tag from .models import ChatMessage, ProjectionDefault, Projector, Tag
class JSONSerializerField(Field): class JSONSerializerField(Field):
@ -22,15 +22,26 @@ class JSONSerializerField(Field):
return data return data
class ProjectionDefaultSerializer(ModelSerializer):
"""
Serializer for core.models.ProjectionDefault objects.
"""
class Meta:
model = ProjectionDefault
fields = ('id', 'name', 'display_name', 'projector', )
class ProjectorSerializer(ModelSerializer): class ProjectorSerializer(ModelSerializer):
""" """
Serializer for core.models.Projector objects. Serializer for core.models.Projector objects.
""" """
config = JSONSerializerField(write_only=True) config = JSONSerializerField(write_only=True)
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Projector model = Projector
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'width', 'height',) fields = ('id', 'config', 'elements', 'scale', 'scroll', 'name', 'blank', 'width', 'height', 'projectiondefaults', )
read_only_fields = ('scale', 'scroll', 'blank', 'width', 'height', )
class TagSerializer(ModelSerializer): class TagSerializer(ModelSerializer):

View File

@ -3,6 +3,8 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.dispatch import Signal from django.dispatch import Signal
from .models import ProjectionDefault, Projector
# This signal is sent when the migrate command is done. That means it is sent # This signal is sent when the migrate command is done. That means it is sent
# after post_migrate sending and creating all Permission objects. Don't use it # after post_migrate sending and creating all Permission objects. Don't use it
# for other things than dealing with Permission objects. # for other things than dealing with Permission objects.
@ -20,3 +22,58 @@ def delete_django_app_permissions(sender, **kwargs):
Q(app_label='sessions')) Q(app_label='sessions'))
for permission in Permission.objects.filter(content_type__in=contenttypes): for permission in Permission.objects.filter(content_type__in=contenttypes):
permission.delete() permission.delete()
def create_builtin_projection_defaults(**kwargs):
"""
Creates the builtin defaults:
- agenda_all_items, agenda_list_of_speakers, agenda_current_list_of_speakers
- topics
- assignments
- mediafiles
- motion
- users
These strings have to be used in the controllers where you want to
define a projector button. Use the string to get the id of the
responsible projector and pass this id to the projector button directive.
"""
# Check whether ProjectionDefault objects exist.
if ProjectionDefault.objects.all().exists():
# Do completely nothing if some defaults are already in the database.
return
default_projector = Projector.objects.get(pk=1)
ProjectionDefault.objects.create(
name='agenda_all_items',
display_name='Agenda',
projector=default_projector)
ProjectionDefault.objects.create(
name='topics',
display_name='Topics',
projector=default_projector)
ProjectionDefault.objects.create(
name='agenda_list_of_speakers',
display_name='List of speakers',
projector=default_projector)
ProjectionDefault.objects.create(
name='agenda_current_list_of_speakers',
display_name='Current list of speakers',
projector=default_projector)
ProjectionDefault.objects.create(
name='motions',
display_name='Motions',
projector=default_projector)
ProjectionDefault.objects.create(
name='assignments',
display_name='Elections',
projector=default_projector)
ProjectionDefault.objects.create(
name='users',
display_name='Participants',
projector=default_projector)
ProjectionDefault.objects.create(
name='mediafiles',
display_name='Files',
projector=default_projector)

View File

@ -270,6 +270,12 @@ img {
float: right; float: right;
} }
.col1 .header .submenu > div {
display: inline-block;
float: left;
margin-left: 5px;
}
.col1 .meta .title { .col1 .meta .title {
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
@ -331,15 +337,6 @@ img {
margin-left: 20px; margin-left: 20px;
} }
/* .resolution can be removed with the multiprojector, but maybe
* it could be reused for the settings in the projectormanage-view */
.col1 .input-group .resolution {
float: none;
display: inline-block;
width: 80px;
border-radius: 4px !important;
}
/* Toolbar to save motion in inline editing mode */ /* Toolbar to save motion in inline editing mode */
.motion-save-toolbar { .motion-save-toolbar {
position: fixed; position: fixed;
@ -586,7 +583,6 @@ img {
#content .col2 .section a:hover { #content .col2 .section a:hover {
text-decoration: none; text-decoration: none;
opacity: 0.5;
} }
#content .toggle-icon { #content .toggle-icon {
@ -652,20 +648,20 @@ img {
color: #CC0000; color: #CC0000;
} }
.col2 .notNull { .notNull {
color: red; color: red;
font-weight: bold; font-weight: bold;
} }
/* iframe for live view */ /* iframe for live view */
.col2 #iframe { .iframe {
-moz-transform-origin: 0 0; -moz-transform-origin: 0 0;
-webkit-transform-origin: 0 0; -webkit-transform-origin: 0 0;
-o-transform-origin: 0 0; -o-transform-origin: 0 0;
transform-origin: 0 0 0; transform-origin: 0 0 0;
} }
.col2 #iframewrapper { .iframewrapper {
width: 256px; width: 256px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -673,7 +669,7 @@ img {
margin-bottom: 10px; margin-bottom: 10px;
} }
.col2 #iframeoverlay { .iframeoverlay {
width: 256px; width: 256px;
position: absolute; position: absolute;
top: 0px; top: 0px;
@ -782,6 +778,77 @@ img {
margin-bottom: 5px margin-bottom: 5px
} }
/** Pojector sidebar **/
.col2 .projectorSelector {
margin-top: 10px;
margin-bottom: 10px;
}
.col2 .projectorSelector > div > div {
width: 65%;
padding-right: 5px;
float: left;
}
.col2 .projectorSelector > div > div > button {
width: 100%;
}
.col2 .projectorSelector .manageBtn {
width: 35%;
}
.col2 .projectorSelector .btn-group {
margin-left: auto;
margin-right: auto;
display: table;
}
.col2 .projectorSelector > div {
margin-bottom: 10px;
}
/** manage-projectors **/
.projectorContainer > div {
display: inline-table;
width: 256px;
margin: 10px 20px 35px 10px;
}
.projectorContainer > div > div {
margin-bottom: 10px;
}
.projectorContainer .middle {
width: 100%;
}
.projectorContainer .middle > div {
margin-left: auto;
margin-right: auto;
display: table;
}
.projectorContainer .dropdown {
width: 65%;
padding-right: 5px;
float: left;
}
.projectorContainer .dropdown > button {
width: 100%;
}
.projectorContainer .btn-danger {
width: 35%;
}
.projectorContainer .resolution {
display: inline-block;
width: 120px;
}
/** Footer **/ /** Footer **/
#footer { #footer {
float: left; float: left;
@ -988,21 +1055,6 @@ img {
} }
/* List of speakers view */ /* List of speakers view */
.fullscreendiv {
position: absolute;
font-size:120%;
top:0;
bottom:0;
right:0;
left:0;
padding: 50px;
background: #fff;
overflow: hidden;
width: 100vw;
height: 100vh;
z-index: 99;
}
.fullscreendiv-title { .fullscreendiv-title {
border-bottom: 5px solid #d3d3d3; border-bottom: 5px solid #d3d3d3;
margin-bottom: 40px; margin-bottom: 40px;

View File

@ -3,7 +3,11 @@
* *
*/ */
body{ html, body {
height: 100%;
}
body {
font-size: 20px !important; font-size: 20px !important;
line-height: 24px !important; line-height: 24px !important;
overflow: hidden; overflow: hidden;
@ -34,7 +38,7 @@ body{
transform-origin: 0 0 0; transform-origin: 0 0 0;
} }
.pContainer #iframewrapper { .pContainer #iframewrapper, .pContainer .error {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
margin-left: auto; margin-left: auto;
@ -49,6 +53,12 @@ body{
z-index: 1; z-index: 1;
} }
.pContainer .error > p {
color: #f00;
font-size: 150%;
text-align: center;
}
/*** HEADER ***/ /*** HEADER ***/
#header { #header {
box-shadow: 0 0 7px rgba(0,0,0,0.6); box-shadow: 0 0 7px rgba(0,0,0,0.6);
@ -278,6 +288,10 @@ hr {
line-height: normal !important; line-height: normal !important;
z-index: 301; z-index: 301;
} }
.identify {
background-color: #D9F8C4;
z-index: 400;
}
/*** PDF presentation ***/ /*** PDF presentation ***/
.rotate0 { .rotate0 {
@ -379,7 +393,6 @@ tr.elected td {
} }
/*** Line numbers ***/ /*** Line numbers ***/
.motion-text .highlight { .motion-text .highlight {
background-color: #ff0; background-color: #ff0;

View File

@ -33,11 +33,20 @@ angular.module('OpenSlidesApp.core', [
} }
]) ])
.factory('ProjectorID', [
function () {
return function () {
return /projector\/(\d+)\//.exec(location.pathname)[1];
};
}
])
.factory('autoupdate', [ .factory('autoupdate', [
'DS', 'DS',
'$rootScope', '$rootScope',
'REALM', 'REALM',
function (DS, $rootScope, REALM) { 'ProjectorID',
function (DS, $rootScope, REALM, ProjectorID) {
var socket = null; var socket = null;
var recInterval = null; var recInterval = null;
$rootScope.connected = false; $rootScope.connected = false;
@ -46,8 +55,7 @@ angular.module('OpenSlidesApp.core', [
if (REALM == 'site') { if (REALM == 'site') {
websocketPath = '/ws/site/'; websocketPath = '/ws/site/';
} else if (REALM == 'projector') { } else if (REALM == 'projector') {
// TODO: At the moment there is only one projector. Find out which one is requested websocketPath = '/ws/projector/' + ProjectorID() + '/';
websocketPath = '/ws/projector/1/';
} else { } else {
console.error('The constant REALM is not set properly.'); console.error('The constant REALM is not set properly.');
} }
@ -184,10 +192,12 @@ angular.module('OpenSlidesApp.core', [
autoupdate.onMessage(function(json) { autoupdate.onMessage(function(json) {
// TODO: when MODEL.find() is called after this // TODO: when MODEL.find() is called after this
// a new request is fired. This could be a bug in DS // a new request is fired. This could be a bug in DS
// TODO: If you don't have the permission to see a projector, the var dataList = [];
// variable json is a string with an error message. Therefor try {
// the next line fails. dataList = JSON.parse(json);
var dataList = JSON.parse(json); } catch(err) {
console.error(json);
}
_.forEach(dataList, function(data) { _.forEach(dataList, function(data) {
console.log("Received object: " + data.collection + ", " + data.id); console.log("Received object: " + data.collection + ", " + data.id);
var instance = DS.get(data.collection, data.id); var instance = DS.get(data.collection, data.id);
@ -246,7 +256,7 @@ angular.module('OpenSlidesApp.core', [
return function () { return function () {
Config.findAll(); Config.findAll();
// Loads all projector data // Loads all projector data and the projectiondefaults
Projector.findAll(); Projector.findAll();
// Loads all chat messages data and their user_ids // Loads all chat messages data and their user_ids
@ -305,34 +315,93 @@ angular.module('OpenSlidesApp.core', [
} }
]) ])
/*
* This places a projector button in the document.
*
* Example: <projector-button model="motion" default-projector.id="defPrId"
* additional-id="2" content="{{ 'project' | translate }}"></projector-button>
* This button references to model (in this example 'motion'). Also a defaultProjectionId
* has to be given. In the example it's a scope variable. The next two parameters are additional:
* - additional-id: Then the model.project and model.isProjected will be called with
* this argument (e. g.: model.project(2))
* - content: A text placed behind the projector symbol.
*/
.directive('projectorButton', [
'Projector',
function (Projector) {
return {
restrict: 'E',
templateUrl: 'static/templates/projector-button.html',
link: function (scope, element, attributes) {
if (!attributes.model) {
throw 'A model has to be given!';
} else if (!attributes.defaultProjectorId) {
throw 'A default-projector-id has to be given!';
}
Projector.bindAll({}, scope, 'projectors');
scope.$watch(attributes.model, function (model) {
scope.model = model;
});
scope.$watch(attributes.defaultProjectorId, function (defaultProjectorId) {
scope.defaultProjectorId = defaultProjectorId;
});
if (attributes.additionalId) {
scope.$watch(attributes.additionalId, function (id) {
scope.additionalId = id;
});
}
if (attributes.content) {
attributes.$observe('content', function (content) {
scope.content = content;
});
}
}
};
}
])
.factory('jsDataModel', [ .factory('jsDataModel', [
'$http', '$http',
'Projector', 'Projector',
function($http, Projector) { function($http, Projector) {
var BaseModel = function() {}; var BaseModel = function() {};
BaseModel.prototype.project = function() { BaseModel.prototype.project = function(projectorId) {
// if this object is already projected on projectorId, delete this element from this projector
var isProjectedId = this.isProjected();
if (isProjectedId > 0) {
$http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
// if it was the same projector before, just delete it but not show again
if (isProjectedId != projectorId) {
return $http.post( return $http.post(
'/rest/core/projector/1/prune_elements/', '/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: this.getResourceName(), id: this.id}] [{name: this.getResourceName(), id: this.id}]
); );
}
}; };
BaseModel.prototype.isProjected = function() { BaseModel.prototype.isProjected = function() {
// Returns true if there is a projector element with the same // Returns the projector id if there is a projector element
// name and the same id. // with the same name and the same id. Else returns 0.
var projector = Projector.get(1); // Attention: if this element is projected multiple times, only the
var isProjected; // id of the last projector is returned.
if (typeof projector !== 'undefined') {
var self = this; var self = this;
var predicate = function (element) { var predicate = function (element) {
return element.name == self.getResourceName() && return element.name == self.getResourceName() &&
typeof element.id !== 'undefined' && typeof element.id !== 'undefined' &&
element.id == self.id; element.id == self.id;
}; };
isProjected = typeof _.findKey(projector.elements, predicate) === 'string'; var isProjectedId = 0;
} else { Projector.getAll().forEach(function (projector) {
isProjected = false; if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjectedId = projector.id;
} }
return isProjected; });
return isProjectedId;
}; };
return BaseModel; return BaseModel;
} }
@ -396,10 +465,74 @@ angular.module('OpenSlidesApp.core', [
*/ */
.factory('Projector', [ .factory('Projector', [
'DS', 'DS',
function(DS) { '$http',
'Config',
function(DS, $http, Config) {
return DS.defineResource({ return DS.defineResource({
name: 'core/projector', name: 'core/projector',
onConflict: 'replace', onConflict: 'replace',
relations: {
hasMany: {
'core/projectiondefault': {
localField: 'projectiondefaults',
foreignKey: 'projector_id',
}
},
},
methods: {
controlProjector: function(action, direction) {
$http.post('/rest/core/projector/' + this.id + '/control_view/',
{"action": action, "direction": direction}
);
},
getStateForCurrentSlide: function () {
var return_dict;
angular.forEach(this.elements, function(key, value) {
if (value.name == 'agenda/list-of-speakers') {
return_dict = {
'state': 'agenda.item.detail',
'param': {id: value.id}
};
} else if (
value.name != 'agenda/item-list' &&
value.name != 'core/clock' &&
value.name != 'core/countdown' &&
value.name != 'core/message' ) {
return_dict = {
'state': value.name.replace('/', '.')+'.detail.update',
'param': {id: value.id}
};
}
});
return return_dict;
},
toggleBlank: function () {
$http.post('/rest/core/projector/' + this.id + '/control_blank/',
!this.blank
);
},
toggleBroadcast: function () {
$http.post('/rest/core/projector/' + this.id + '/broadcast/');
}
},
});
}
])
/* Model for all projection defaults */
.factory('ProjectionDefault', [
'DS',
function(DS) {
return DS.defineResource({
name: 'core/projectiondefault',
relations: {
belongsTo: {
'core/projector': {
localField: 'projector',
localKey: 'projector_id',
}
}
}
}); });
} }
]) ])
@ -463,11 +596,13 @@ angular.module('OpenSlidesApp.core', [
} }
]) ])
// This filter filters all items in array. If the filterArray is empty, the array is passed. /*
// The filterArray contains numbers of the multiselect: [1, 3, 4]. * This filter filters all items in an array. If the filterArray is empty, the
// Then, all items in array are passed, if the item_id (get with id_function) matches one of the * array is passed. The filterArray contains numbers of the multiselect, e. g. [1, 3, 4].
// ids in filterArray. id_function could also return a list of ids. Example: * Then, all items in the array are passed, if the item_id (get with id_function) matches
// Item 1 has two tags with ids [1, 4]. filterArray = [3, 4] --> match * one of the ids in filterArray. id_function could also return a list of ids. Example:
* Item 1 has two tags with ids [1, 4]. filterArray == [3, 4] --> match
*/
.filter('SelectMultipleFilter', [ .filter('SelectMultipleFilter', [
function () { function () {
return function (array, filterArray, idFunction) { return function (array, filterArray, idFunction) {
@ -522,8 +657,9 @@ angular.module('OpenSlidesApp.core', [
'ChatMessage', 'ChatMessage',
'Config', 'Config',
'Projector', 'Projector',
'ProjectionDefault',
'Tag', 'Tag',
function (ChatMessage, Config, Projector, Tag) {} function (ChatMessage, Config, Projector, ProjectionDefault, Tag) {}
]); ]);
}()); }());

View File

@ -59,24 +59,29 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Projector Container Controller // Projector Container Controller
.controller('ProjectorContainerCtrl', [ .controller('ProjectorContainerCtrl', [
'$scope', '$scope',
'Config', '$location',
'gettext',
'loadGlobalData', 'loadGlobalData',
function($scope, Config, loadGlobalData) { 'Projector',
'ProjectorID',
function($scope, $location, gettext, loadGlobalData, Projector, ProjectorID) {
loadGlobalData(); loadGlobalData();
// watch for changes in Config
var last_conf; $scope.projector_id = ProjectorID();
$scope.error = '';
// watch for changes in Projector
$scope.$watch(function () { $scope.$watch(function () {
return Config.lastModified(); return Projector.lastModified($scope.projector_id);
}, function () { }, function () {
// With multiprojector, get the resolution from Prjector.get(pk).{width; height} var projector = Projector.get($scope.projector_id);
if (typeof $scope.config === 'function') { if (projector) {
var conf = $scope.config('projector_resolution'); $scope.error = '';
if(!last_conf || last_conf.width != conf.width || last_conf.height != conf.height) { $scope.projectorWidth = projector.width;
last_conf = conf; $scope.projectorHeight = projector.height;
$scope.projectorWidth = conf.width;
$scope.projectorHeight = conf.height;
$scope.recalculateIframe(); $scope.recalculateIframe();
} } else {
$scope.error = gettext('Can not open the projector.');
} }
}); });
@ -114,16 +119,17 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.controller('ProjectorCtrl', [ .controller('ProjectorCtrl', [
'$scope', '$scope',
'$location',
'Projector', 'Projector',
'slides', 'slides',
function($scope, Projector, slides) { 'Config',
$scope.$watch(function () { 'ProjectorID',
// TODO: Use the current projector. At the moment there is only one. function($scope, $location, Projector, slides, Config, ProjectorID) {
return Projector.lastModified(1); var projector_id = ProjectorID();
}, function () {
// TODO: Use the current projector. At the moment there is only one $scope.broadcast = 0;
var projector = Projector.get(1);
if (projector) { var setElements = function (projector) {
$scope.elements = []; $scope.elements = [];
_.forEach(slides.getElements(projector), function(element) { _.forEach(slides.getElements(projector), function(element) {
if (!element.error) { if (!element.error) {
@ -132,9 +138,64 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
console.error("Error for slide " + element.name + ": " + element.error); console.error("Error for slide " + element.name + ": " + element.error);
} }
}); });
// TODO: Use the current projector. At the moment there is only one };
$scope.scroll = -80 * Projector.get(1).scroll;
$scope.scale = 100 + 20 * Projector.get(1).scale; $scope.$watch(function () {
return Projector.lastModified(projector_id);
}, function () {
$scope.projector = Projector.get(projector_id);
if ($scope.projector) {
if ($scope.broadcast === 0) {
setElements($scope.projector);
$scope.blank = $scope.projector.blank;
}
} else {
// Blank projector on error
$scope.elements = [];
$scope.projector = {
scroll: 0,
scale: 0,
blank: true
};
}
});
$scope.$watch(function () {
return Config.lastModified('projector_broadcast');
}, function () {
var bc = Config.get('projector_broadcast');
if (bc) {
if ($scope.broadcast != bc.value) {
$scope.broadcast = bc.value;
if ($scope.broadcastDeregister) {
// revert to original $scope.projector
$scope.broadcastDeregister();
$scope.broadcastDeregister = null;
setElements($scope.projector);
$scope.blank = $scope.projector.blank;
}
}
if ($scope.broadcast > 0) {
// get elements and blank from broadcast projector
$scope.broadcastDeregister = $scope.$watch(function () {
return Projector.lastModified($scope.broadcast);
}, function () {
if ($scope.broadcast > 0) {
var broadcast_projector = Projector.get($scope.broadcast);
if (broadcast_projector) {
setElements(broadcast_projector);
$scope.blank = broadcast_projector.blank;
}
}
});
}
}
});
$scope.$on('$destroy', function() {
if ($scope.broadcastDeregister) {
$scope.broadcastDeregister();
$scope.broadcastDeregister = null;
} }
}); });
} }
@ -158,13 +219,14 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset ); $scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
$scope.status = $scope.element.status; $scope.running = $scope.element.running;
$scope.visible = $scope.element.visible; $scope.visible = $scope.element.visible;
$scope.selected = $scope.element.selected;
$scope.index = $scope.element.index; $scope.index = $scope.element.index;
$scope.description = $scope.element.description; $scope.description = $scope.element.description;
// start interval timer if countdown status is running // start interval timer if countdown status is running
var interval; var interval;
if ($scope.status == "running") { if ($scope.running) {
interval = $interval( function() { interval = $interval( function() {
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset ); $scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
}, 1000); }, 1000);
@ -186,6 +248,8 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// class. // class.
$scope.message = $scope.element.message; $scope.message = $scope.element.message;
$scope.visible = $scope.element.visible; $scope.visible = $scope.element.visible;
$scope.selected = $scope.element.selected;
$scope.type = $scope.element.type;
} }
]); ]);

View File

@ -717,7 +717,7 @@ angular.module('OpenSlidesApp.core.site', [
templateUrl: 'static/templates/home.html' templateUrl: 'static/templates/home.html'
}) })
.state('projector', { .state('projector', {
url: '/projector', url: '/projector/{id:int}',
templateUrl: 'static/templates/projector-container.html', templateUrl: 'static/templates/projector-container.html',
data: {extern: true}, data: {extern: true},
onEnter: function($window) { onEnter: function($window) {
@ -725,13 +725,18 @@ angular.module('OpenSlidesApp.core.site', [
} }
}) })
.state('real-projector', { .state('real-projector', {
url: '/real-projector', url: '/real-projector/{id:int}',
templateUrl: 'static/templates/projector.html', templateUrl: 'static/templates/projector.html',
data: {extern: true}, data: {extern: true},
onEnter: function($window) { onEnter: function($window) {
$window.location.href = this.url; $window.location.href = this.url;
} }
}) })
.state('manage-projectors', {
url: '/manage-projectors',
templateUrl: 'static/templates/core/manage-projectors.html',
controller: 'ManageProjectorsCtrl'
})
.state('core', { .state('core', {
url: '/core', url: '/core',
abstract: true, abstract: true,
@ -901,7 +906,6 @@ angular.module('OpenSlidesApp.core.site', [
'Config', 'Config',
'gettextCatalog', 'gettextCatalog',
function($parse, Config, gettextCatalog) { function($parse, Config, gettextCatalog) {
// remove resolution when changing to multiprojector
function getHtmlType(type) { function getHtmlType(type) {
return { return {
string: 'text', string: 'text',
@ -911,7 +915,6 @@ angular.module('OpenSlidesApp.core.site', [
choice: 'choice', choice: 'choice',
colorpicker: 'colorpicker', colorpicker: 'colorpicker',
comments: 'comments', comments: 'comments',
resolution: 'resolution',
}[type]; }[type];
} }
@ -1187,209 +1190,439 @@ angular.module('OpenSlidesApp.core.site', [
'$http', '$http',
'$interval', '$interval',
'$state', '$state',
'$q',
'Config', 'Config',
'Projector', 'Projector',
function($scope, $http, $interval, $state, Config, Projector) { function($scope, $http, $interval, $state, $q, Config, Projector) {
// bind projector elements to the scope, update after projector changed $scope.countdowns = [];
$scope.highestCountdownIndex = 0;
$scope.messages = [];
$scope.highestMessageIndex = 0;
var cancelIntervalTimers = function () {
$scope.countdowns.forEach(function (countdown) {
$interval.cancel(countdown.interval);
});
};
// Get all message and countdown data from the defaultprojector (id=1)
var rebuildAllElements = function () {
$scope.countdowns = [];
$scope.messages = [];
_.forEach(Projector.get(1).elements, function (element, uuid) {
if (element.name == 'core/countdown') {
$scope.countdowns.push(element);
if (element.running) {
// calculate remaining seconds directly because interval starts with 1 second delay
$scope.calculateCountdownTime(element);
// start interval timer (every second)
element.interval = $interval(function () { $scope.calculateCountdownTime(element); }, 1000);
} else {
element.seconds = element.countdown_time;
}
if (element.index > $scope.highestCountdownIndex) {
$scope.highestCountdownIndex = element.index;
}
} else if (element.name == 'core/message') {
$scope.messages.push(element);
if (element.index > $scope.highestMessageIndex) {
$scope.highestMessageIndex = element.index;
}
}
});
};
$scope.$watch(function () { $scope.$watch(function () {
return Projector.lastModified(1); return Projector.lastModified();
}, function () { }, function () {
$scope.projectors = Projector.getAll();
if (!$scope.active_projector) {
$scope.changeProjector($scope.projectors[0]);
}
// stop ALL interval timer // stop ALL interval timer
for (var i=0; i<$scope.countdowns.length; i++) { cancelIntervalTimers();
if ( $scope.countdowns[i].interval ) {
$interval.cancel($scope.countdowns[i].interval); rebuildAllElements();
}
}
// rebuild all variables after projector update
$scope.rebuildAllElements();
}); });
$scope.$on('$destroy', function() { $scope.$on('$destroy', function() {
// Cancel all intervals if the controller is destroyed // Cancel all intervals if the controller is destroyed
for (var i=0; i<$scope.countdowns.length; i++) { cancelIntervalTimers();
if ( $scope.countdowns[i].interval ) {
$interval.cancel($scope.countdowns[i].interval);
}
}
}); });
// watch for changes in Config // watch for changes in projector_broadcast
var last_conf; var last_broadcast;
$scope.$watch(function () { $scope.$watch(function () {
return Config.lastModified(); return Config.lastModified();
}, function () { }, function () {
var conf = Config.get('projector_resolution').value; var broadcast = Config.get('projector_broadcast').value;
// With multiprojector, get the resolution from Prjector.get(pk).{width; height} if (!last_broadcast || last_broadcast != broadcast) {
if(!last_conf || last_conf.width != conf.width || last_conf.height != conf.height) { last_broadcast = broadcast;
last_conf = conf; $scope.broadcast = broadcast;
$scope.projectorWidth = conf.width;
$scope.projectorHeight = conf.height;
$scope.scale = 256.0 / $scope.projectorWidth;
$scope.iframeHeight = $scope.scale * $scope.projectorHeight;
} }
}); });
$scope.changeProjector = function (projector) {
$scope.active_projector = projector;
$scope.scale = 256.0 / projector.width;
$scope.iframeHeight = $scope.scale * projector.height;
};
$scope.editCurrentSlide = function (projector) {
var state = projector.getStateForCurrentSlide();
if (state) {
$state.go(state.state, state.param);
}
};
// *** countdown functions *** // *** countdown functions ***
$scope.calculateCountdownTime = function (countdown) { $scope.calculateCountdownTime = function (countdown) {
countdown.seconds = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset ); countdown.seconds = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
}; };
$scope.rebuildAllElements = function () { $scope.editCountdown = function (countdown) {
$scope.countdowns = []; countdown.editFlag = false;
$scope.messages = []; $scope.projectors.forEach(function (projector) {
// iterate via all projector elements and catch all countdowns and messages _.forEach(projector.elements, function (element, uuid) {
$.each(Projector.get(1).elements, function(key, value) { if (element.name == 'core/countdown' && element.index == countdown.index) {
if (value.name == 'core/countdown') { var data = {};
$scope.countdowns.push(value); data[uuid] = {
if (value.status == "running") { "description": countdown.description,
// calculate remaining seconds directly because interval starts with 1 second delay "default_time": parseInt(countdown.default_time)
$scope.calculateCountdownTime(value); };
// start interval timer (every second) if (!countdown.running) {
value.interval = $interval( function() { $scope.calculateCountdownTime(value); }, 1000); data[uuid].countdown_time = parseInt(countdown.default_time);
} else {
value.seconds = value.countdown_time;
} }
} $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
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 () { $scope.addCountdown = function () {
var defaultvalue = parseInt(Config.get('projector_default_countdown').value); var default_time = parseInt($scope.config('projector_default_countdown'));
$http.post('/rest/core/projector/1/activate_elements/', [{ $scope.highestCountdownIndex++;
// select all projectors on creation, so write the countdown to all projectors
$scope.projectors.forEach(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/countdown', name: 'core/countdown',
status: 'stop', countdown_time: default_time,
default_time: default_time,
visible: false, visible: false,
index: $scope.countdowns.length, selected: true,
countdown_time: defaultvalue, index: $scope.highestCountdownIndex,
default: defaultvalue, running: false,
stable: true stable: true,
}]); }]);
});
}; };
$scope.removeCountdown = function (countdown) { $scope.removeCountdown = function (countdown) {
var data = {}; $scope.projectors.forEach(function (projector) {
var delta = 0; var countdowns = [];
// rebuild index for all countdowns after the selected (deleted) countdown _.forEach(projector.elements, function (element, uuid) {
for (var i=0; i<$scope.countdowns.length; i++) { if (element.name == 'core/countdown' && element.index == countdown.index) {
if ( $scope.countdowns[i].uuid == countdown.uuid ) { $http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [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) { $scope.startCountdown = function (countdown) {
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/countdown' && element.index == countdown.index) {
var data = {}; var data = {};
// calculate end point of countdown (in seconds!) // calculate end point of countdown (in seconds!)
var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time; var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time;
data[countdown.uuid] = { data[uuid] = {
"status": "running", 'running': true,
"countdown_time": endTimestamp 'countdown_time': endTimestamp
}; };
$http.post('/rest/core/projector/1/update_elements/', data); $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
}; };
$scope.stopCountdown = function (countdown) { $scope.stopCountdown = function (countdown) {
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/countdown' && element.index == countdown.index) {
var data = {}; var data = {};
// calculate rest duration of countdown (in seconds!) // calculate rest duration of countdown (in seconds!)
var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset ); var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
data[countdown.uuid] = { data[uuid] = {
"status": "stop", 'running': false,
"countdown_time": newDuration 'countdown_time': newDuration
}; };
$http.post('/rest/core/projector/1/update_elements/', data); $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
}; };
$scope.resetCountdown = function (countdown) { $scope.resetCountdown = function (countdown) {
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/countdown' && element.index == countdown.index) {
var data = {}; var data = {};
data[countdown.uuid] = { data[uuid] = {
"status": "stop", 'running': false,
"countdown_time": countdown.default, 'countdown_time': countdown.default_time,
}; };
$http.post('/rest/core/projector/1/update_elements/', data); $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
}; };
// *** message functions *** // *** 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) { $scope.editMessage = function (message) {
message.editFlag = false;
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/message' && element.index == message.index) {
var data = {}; var data = {};
data[message.uuid] = { data[uuid] = {
"message": message.message, message: message.message,
}; };
$http.post('/rest/core/projector/1/update_elements/', data); $http.post('/rest/core/projector/' + projector.id + '/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 == 'agenda/list-of-speakers') {
$state.go('agenda.item.detail', {id: value.id});
} else if (
value.name != 'agenda/item-list' &&
value.name != 'core/clock' &&
value.name != 'core/countdown' &&
value.name != 'core/message' ) {
$state.go(value.name.replace('/', '.')+'.detail.update', {id: value.id});
} }
}); });
});
};
$scope.addMessage = function () {
$scope.highestMessageIndex++;
// select all projectors on creation, so write the countdown to all projectors
$scope.projectors.forEach(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/message',
visible: false,
selected: true,
index: $scope.highestMessageIndex,
message: '',
stable: true,
}]);
});
};
$scope.removeMessage = function (message) {
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/message' && element.index == message.index) {
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
}
});
});
};
/* project functions*/
$scope.project = function (element) {
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
var data = {};
data[uuid] = {visible: !projectorElement.visible};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
$scope.isProjected = function (element) {
var projectorIds = [];
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
if (projectorElement.visible && projectorElement.selected) {
projectorIds.push(projector.id);
}
}
});
});
return projectorIds;
};
$scope.isProjectedOn = function (element, projector) {
var projectedIds = $scope.isProjected(element);
return _.indexOf(projectedIds, projector.id) > -1;
};
$scope.hasProjector = function (element, projector) {
var hasProjector = false;
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
if (projectorElement.selected) {
hasProjector = true;
}
}
});
return hasProjector;
};
$scope.toggleProjector = function (element, projector) {
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
var data = {};
data[uuid] = {
'selected': !projectorElement.selected,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
};
$scope.selectAll = function (element, value) {
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
var data = {};
data[uuid] = {
'selected': value,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
$scope.preventClose = function (e) {
e.stopPropagation();
};
}
])
.controller('ManageProjectorsCtrl', [
'$scope',
'$http',
'$state',
'$timeout',
'Projector',
'ProjectionDefault',
'Config',
'gettextCatalog',
function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, gettextCatalog) {
ProjectionDefault.bindAll({}, $scope, 'projectiondefaults');
// watch for changes in projector_broadcast
// and projector_currentListOfSpeakers_reference
var last_broadcast, last_clos;
$scope.$watch(function () {
return Config.lastModified();
}, function () {
var broadcast = $scope.config('projector_broadcast'),
currentListOfSpeakers = $scope.config('projector_currentListOfSpeakers_reference');
if (!last_broadcast || last_broadcast != broadcast) {
last_broadcast = broadcast;
$scope.broadcast = broadcast;
}
if (!last_clos || last_clos != currentListOfSpeakers) {
last_clos = currentListOfSpeakers;
$scope.currentListOfSpeakers = currentListOfSpeakers;
}
});
// watch for changes in Projector, and recalc scale and iframeHeight
var first_watch = true;
$scope.resolutions = [];
$scope.edit = [];
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
$scope.projectors = Projector.getAll();
$scope.projectors.forEach(function (projector) {
projector.iframeScale = 256.0 / projector.width;
projector.iframeHeight = projector.iframeScale * projector.height;
if (first_watch) {
$scope.resolutions[projector.id] = {
width: projector.width,
height: projector.height
};
$scope.edit[projector.id] = false;
}
});
if ($scope.projectors.length) {
first_watch = false;
}
});
// Set list of speakers reference
$scope.setListOfSpeakers = function (projector) {
Config.get('projector_currentListOfSpeakers_reference').value = projector.id;
Config.save('projector_currentListOfSpeakers_reference');
};
// Projector functions
$scope.setProjectionDefault = function (projector, def) {
$http.post('/rest/core/projector/' + projector.id + '/set_projectiondefault/', def.id);
};
$scope.createProjector = function (name) {
var projector = {
name: name,
config: {},
scale: 0,
scroll: 0,
blank: false,
projectiondefaults: [],
};
Projector.create(projector).then(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/clock',
stable: true
}]);
$scope.resolutions[projector.id] = {
width: projector.width,
height: projector.height
};
});
};
$scope.deleteProjector = function (projector) {
if (projector.id != 1) {
Projector.destroy(projector.id);
}
};
$scope.editCurrentSlide = function (projector) {
var state = projector.getStateForCurrentSlide();
if (state) {
$state.go(state.state, state.param);
}
};
$scope.editName = function (projector) {
projector.config = projector.elements;
Projector.save(projector);
};
$scope.changeResolution = function (projectorId) {
$http.post(
'/rest/core/projector/' + projectorId + '/set_resolution/',
$scope.resolutions[projectorId]
).then(function (success) {
$scope.resolutions[projectorId].error = null;
}, function (error) {
$scope.resolutions[projectorId].error = error.data.detail;
});
};
// Identify projectors
$scope.identifyProjectors = function () {
if ($scope.identifyPromise) {
$timeout.cancel($scope.identifyPromise);
$scope.removeIdentifierMessages();
} else {
$scope.projectors.forEach(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/message',
stable: true,
selected: true,
visible: true,
message: gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + projector.name,
type: 'identify'
}]);
});
$scope.identifyPromise = $timeout($scope.removeIdentifierMessages, 3000);
}
};
$scope.removeIdentifierMessages = function () {
Projector.getAll().forEach(function (projector) {
angular.forEach(projector.elements, function (uuid, value) {
if (value.name == 'core/message' && value.type == 'identify') {
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
}
});
});
$scope.identifyPromise = null;
}; };
} }
]) ])

View File

@ -11,26 +11,6 @@
id="{{ key }}" id="{{ key }}"
type="{{ type }}"> type="{{ type }}">
<!-- resolution -->
<!-- Can be removed with multiprojector, but maybe it could be reused in the projectormanage-view -->
<!-- if removed, remember to delete the class resolution -->
<span ng-if="type == 'resolution'">
<translate>Width</translate>:
<input ng-model="$parent.value.width"
ng-model-option="{debounce: 1000}"
ng-change="save(configOption.key, $parent.value)"
class="form-control resolution"
id="{{ key }}_width"
type="number">
<translate>Height</translate>:
<input ng-model="$parent.value.height"
ng-model-option="{debounce: 1000}"
ng-change="save(configOption.key, $parent.value)"
class="form-control resolution"
id="{{ key }}_height"
type="number">
</span>
<!-- comments --> <!-- comments -->
<div class="input-comments" ng-if="type == 'comments'"> <div class="input-comments" ng-if="type == 'comments'">
<div ng-repeat="entry in $parent.value" class="input-group"> <div ng-repeat="entry in $parent.value" class="input-group">

View File

@ -0,0 +1,200 @@
<div class="header">
<div class="title">
<div class="submenu">
<div>
<button class="btn btn-primary" ng-bootbox-prompt="{{ 'Please enter a name for the new projector' | translate }}"
ng-bootbox-prompt-action="createProjector(result)">
<i class="fa fa-plus"></i>
<translate>New</translate>
</button>
</div>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" id="menuListofSpeakers" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<translate>Current list of speakers reference</translate>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-entries" aria-labelledby="menuListOfSpeakers">
<li ng-repeat="projector in projectors"
ng-click="setListOfSpeakers(projector)">
<i class="fa fa-check" ng-if="projector.id == currentListOfSpeakers"></i>
{{ projector.name }}
</li>
</ul>
</div>
<div>
<button class="btn" ng-click="identifyProjectors()" ng-class="identifyPromise ? 'btn-primary' : 'btn-default'">
<i class="fa fa-binoculars"></i>
<translate>Identify</translate>
</button>
</div>
</div>
<h1 translate>Projektoren verwalten</h1>
</div>
</div>
<div class="details">
<div class="projectorContainer">
<div ng-repeat="projector in projectors">
<div>
<a ui-sref="projector({id: projector.id})">
{{ projector.id }}:
<strong>{{ projector.name }}</strong>
</a>
<a href="" class="pull-right" ng-click="edit[projector.id] = !edit[projector.id]"><i class="fa" ng-class="edit[projector.id] ? 'fa-times' : 'fa-pencil'"></i></a>
</div>
<div ng-show="edit[projector.id]" style="margin-bottom: -20px;">
<div>
<div class="dropdown" uib-dropdown>
<button class="btn btn-default btn-sm" id="menuProjector{{ pr.id }}" uib-dropdown-toggle>
<translate>Projection defaults</translate>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-entries" aria-labelledby="menuProjector{{ pr.id }}">
<li ng-repeat="projectiondefault in projectiondefaults | orderBy:'id'"
ng-click="setProjectionDefault(projector, projectiondefault)">
<i class="fa fa-check" ng-if="projectiondefault.projector_id === projector.id"></i>
{{ projectiondefault.display_name | translate }}
</li>
</ul>
</div>
<button type="button" class="btn btn-danger btn-sm"
ng-hide="projector.id==1"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ projector.name }}</b>"
ng-bootbox-confirm-action="deleteProjector(projector)">
<i class="fa fa-trash"></i>
<translate>Delete</translate>
</button>
<div ng-show="projector.id==1" style="height: 30px"><!-- Placeholder --></div>
</div>
<div>
<label for="name{{ projector.id }}" class="control-label"><translate>Name</translate>:</label>
<input type="text" class="form-control" id="name{{ projector.id }}"
ng-model="projector.name" ng-change="editName(projector)"
ng-model-options="{debounce: 2000}"></input>
</div>
<div>
<label for="resolution{{ projector.id }}" class="control-label"><translate>Resolution</translate>:</label>
<div id="resolution{{ projector.id }}">
<input ng-model="resolutions[projector.id].width"
ng-model-option="{debounce: 2000}"
ng-change="changeResolution(projector.id)"
class="form-control resolution"
id="{{ projector.id }}_width"
type="number">
x
<input ng-model="resolutions[projector.id].height"
ng-model-option="{debounce: 2000}"
ng-change="changeResolution(projector.id)"
class="form-control resolution"
id="{{ projector.id }}_height"
type="number">
</div>
<p class="help-block">
{{ resolutions[projector.id].error }}
</p>
</div>
</div>
<style>
#iframe_{{ projector.id }} {
width: {{ projector.width }}px;
height: {{ projector.height }}px;
-moz-transform: scale({{ projector.iframeScale }});
-webkit-transform: scale({{ projector.iframeScale }});
-o-transform: scale({{ projector.iframeScale }});
transform: scale({{ projector.iframeScale }});
/* IE8+ - must be on one line, unfortunately */
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ projector.iframeScale }}, M12=0, M21=0, M22={{ projector.iframeScale }}, SizingMethod='auto expand')";
}
#iframewrapper_{{ projector.id }} {
height: {{ projector.iframeHeight }}px;
}
#iframeoverlay_{{ projector.id }} {
height: {{ projector.iframeHeight }}px;
}
</style>
<a ui-sref="projector({id: projector.id})" target="_blank">
<div class="iframewrapper" id="iframewrapper_{{ projector.id }}">
<iframe class="iframe" id="iframe_{{ projector.id }}" ng-src="{{ '/real-projector/' + projector.id }}" frameborder="0"></iframe>
<div class="iframeoverlay" id="iframeoverlay_{{ projector.id }}"></div>
</div>
</a>
<!-- projector control buttons -->
<div os-perms="core.can_manage_projector">
<!-- edit -->
<a ng-click="editCurrentSlide(projector)"
class="btn btn-default btn-sm"
title="{{ 'Edit current slide' | translate}}">
<i class="fa fa-pencil"></i>
</a>
<!-- scale -->
<div class="btn-group">
<a ng-click="projector.controlProjector('scale', 'down')"
class="btn btn-default btn-sm"
title="{{ 'Smaller' | translate}}">
<i class="fa fa-search-minus"></i>
</a>
<a ng-click="projector.controlProjector('scale', 'up')"
class="btn btn-default btn-sm"
title="{{ 'Bigger' | translate}}">
<i class="fa fa-search-plus"></i>
</a>
<a ng-click="projector.controlProjector('scale', 'reset')"
class="btn btn-default btn-sm"
title="{{ 'Reset scaling' | translate}}">
<i class="fa fa-undo"></i>
</a>
</div>
<span ng-class="{'notNull': projector.scale != 0}">{{ projector.scale }}</span>
<!-- scroll -->
<div class="btn-group">
<a ng-click="projector.controlProjector('scroll', 'down')"
class="btn btn-default btn-sm"
title="{{ 'Scroll up' | translate}}">
<i class="fa fa-arrow-up"></i>
</a>
<a ng-click="projector.controlProjector('scroll', 'up')"
class="btn btn-default btn-sm"
title="{{ 'Scroll down' | translate}}">
<i class="fa fa-arrow-down"></i>
</a>
<a ng-click="projector.controlProjector('scroll', 'reset')"
class="btn btn-default btn-sm"
title="{{ 'Reset scrolling' | translate}}">
<i class="fa fa-undo"></i>
</a>
</div>
<span ng-class="{'notNull': projector.scroll != 0}">{{ projector.scroll }}</span>
</div>
<!-- Default, BC, Blank -->
<div class="middle">
<div class="btn-group">
<button class="btn btn-sm" ng-class="broadcast == projector.id ? 'btn-primary' : 'btn-default'"
ng-click="projector.toggleBroadcast(projector)"
ng-disabled="broadcast > 0 && broadcast != projector.id">
<i class="fa" ng-class="broadcast == projector.id ? 'fa-star' : 'fa-star-o'"></i>
<translate>Broadcast</translate>
</button>
<button class="btn btn-sm" ng-class="projector.blank ? 'btn-primary' : 'btn-default'"
ng-click="projector.toggleBlank(projector)"
ng-disabled="broadcast > 0 && broadcast != projector.id">
<i class="fa" ng-class="projector.blank ? 'fa-square' : 'fa-square-o'"></i>
<translate>Blank</translate>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -10,10 +10,9 @@
</a> </a>
<div uib-collapse="isLiveViewClosed" ng-cloak> <div uib-collapse="isLiveViewClosed" ng-cloak>
<style> <style>
/* iframe for live view */ .col2 #iframe_sidebar {
.col2 #iframe { width: {{ active_projector.width }}px;
width: {{ projectorWidth }}px; height: {{ active_projector.height }}px;
height: {{ projectorHeight }}px;
-moz-transform: scale({{ scale }}); -moz-transform: scale({{ scale }});
-webkit-transform: scale({{ scale }}); -webkit-transform: scale({{ scale }});
-o-transform: scale({{ scale }}); -o-transform: scale({{ scale }});
@ -22,25 +21,72 @@
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ scale }}, M12=0, M21=0, M22={{ scale }}, SizingMethod='auto expand')"; -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ scale }}, M12=0, M21=0, M22={{ scale }}, SizingMethod='auto expand')";
} }
.col2 #iframewrapper { .col2 #iframewrapper_sidebar {
height: {{ iframeHeight }}px; height: {{ iframeHeight }}px;
} }
.col2 #iframeoverlay { .col2 #iframeoverlay_sidebar {
height: {{ iframeHeight }}px; height: {{ iframeHeight }}px;
} }
</style> </style>
<a ui-sref="projector" target="_blank"> <div class="projectorSelector">
<div id="iframewrapper"> <div>
<iframe id="iframe" src="/real-projector" frameborder="0"></iframe> <div class="dropdown" ng-show="projectors.length > 1">
<div id="iframeoverlay"></div> <button class="btn btn-default btn-sm dropdown-toggle" id="menuProjector" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fa fa-video-camera"></i>
{{ active_projector.name }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-entries" aria-labelledby="menuProjector">
<li ng-repeat="projector in projectors"
ng-class="{'projected': projector === active_projector}"
ng-click="changeProjector(projector)">
<i ng-show="projector === active_projector" class="fa fa-video-camera"></i>
{{ projector.name }}
<i ng-show="projector.id == broadcast" class="fa fa-star-o spacer-left"></i>
</li>
</ul>
</div>
<div>
<button class="btn btn-sm" ng-click="active_projector.toggleBlank()" ng-hide="projectors.length > 1"
ng-class="active_projector.blank ? 'btn-primary' : 'btn-default'">
<i class="fa" ng-class="active_projector.blank ? 'fa-square' : 'fa-square-o'"></i>
<translate>Blank</translate>
</button>
</div>
<a class="btn btn-primary btn-sm manageBtn" ui-sref="manage-projectors">
<i class="fa fa-cog fa-lg"></i>
<translate>Manage</translate>
</a>
</div>
<div class="btn-group" ng-show="projectors.length > 1">
<button class="btn btn-sm" ng-class="broadcast == active_projector.id ? 'btn-primary' : 'btn-default'"
ng-click="active_projector.toggleBroadcast()" ng-disabled="broadcast > 0 && broadcast != active_projector.id">
<i class="fa" ng-class="broadcast == active_projector.id ? 'fa-star' : 'fa-star-o'"></i>
<translate>Broadcast</translate>
</button>
<button class="btn btn-sm" ng-click="active_projector.toggleBlank()"
ng-class="active_projector.blank ? 'btn-primary' : 'btn-default'"
ng-disabled="broadcast > 0 && broadcast != active_projector.id">
<i class="fa" ng-class="active_projector.blank ? 'fa-square' : 'fa-square-o'"></i>
<translate>Blank</translate>
</button>
</div>
</div>
<a ui-sref="projector({id: active_projector.id })" target="_blank">
<div class="iframewrapper" id="iframewrapper_sidebar">
<iframe class="iframe" id="iframe_sidebar" ng-src="{{ '/real-projector/' + active_projector.id }}" frameborder="0"></iframe>
<div class="iframeoverlay" id="iframeoverlay_sidebar"></div>
</div> </div>
</a> </a>
<!-- projector control buttons --> <!-- projector control buttons -->
<div os-perms="core.can_manage_projector"> <div os-perms="core.can_manage_projector">
<!-- edit --> <!-- edit -->
<a ng-click="editCurrentSlide()" <a ng-click="editCurrentSlide(active_projector)"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Edit current slide' | translate}}"> title="{{ 'Edit current slide' | translate}}">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
@ -48,43 +94,43 @@
<!-- scale --> <!-- scale -->
<div class="btn-group"> <div class="btn-group">
<a ng-click="controlProjector('scale', 'down')" <a ng-click="active_projector.controlProjector('scale', 'down')"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Smaller' | translate}}"> title="{{ 'Smaller' | translate}}">
<i class="fa fa-search-minus"></i> <i class="fa fa-search-minus"></i>
</a> </a>
<a ng-click="controlProjector('scale', 'up')" <a ng-click="active_projector.controlProjector('scale', 'up')"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Bigger' | translate}}"> title="{{ 'Bigger' | translate}}">
<i class="fa fa-search-plus"></i> <i class="fa fa-search-plus"></i>
</a> </a>
<a ng-click="controlProjector('scale', 'reset')" <a ng-click="active_projector.controlProjector('scale', 'reset')"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Reset scaling' | translate}}"> title="{{ 'Reset scaling' | translate}}">
<i class="fa fa-undo"></i> <i class="fa fa-undo"></i>
</a> </a>
</div> </div>
<span ng-class="{ 'notNull': scaleLevel != 0 }">{{ scaleLevel }}</span> <span ng-class="{ 'notNull': active_projector.scale != 0 }">{{ active_projector.scale }}</span>
<!-- scroll --> <!-- scroll -->
<div class="btn-group"> <div class="btn-group">
<a ng-click="controlProjector('scroll', 'down')" <a ng-click="active_projector.controlProjector('scroll', 'down')"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Scroll up' | translate}}"> title="{{ 'Scroll up' | translate}}">
<i class="fa fa-arrow-up"></i> <i class="fa fa-arrow-up"></i>
</a> </a>
<a ng-click="controlProjector('scroll', 'up')" <a ng-click="active_projector.controlProjector('scroll', 'up')"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Scroll down' | translate}}"> title="{{ 'Scroll down' | translate}}">
<i class="fa fa-arrow-down"></i> <i class="fa fa-arrow-down"></i>
</a> </a>
<a ng-click="controlProjector('scroll', 'reset')" <a ng-click="active_projector.controlProjector('scroll', 'reset')"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Reset scrolling' | translate}}"> title="{{ 'Reset scrolling' | translate}}">
<i class="fa fa-undo"></i> <i class="fa fa-undo"></i>
</a> </a>
</div> </div>
<span ng-class="{ 'notNull': scrollLevel != 0 }">{{ scrollLevel }}</span> <span ng-class="{ 'notNull': active_projector.scroll != 0 }">{{ active_projector.scroll }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -96,11 +142,10 @@
<h4 translate>Countdowns</h4> <h4 translate>Countdowns</h4>
</a> </a>
<div uib-collapse="!isCountdowns" ng-cloak> <div uib-collapse="!isCountdowns" ng-cloak>
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="{{countdown.uuid}}" <div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.uuid}}" class="countdown panel panel-default">
class="countdown panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span ng-if="countdown.description">{{ countdown.description }}</span> <span ng-if="countdown.description">{{ countdown.description }}</span>
<span ng-if="!countdown.description">Countdown {{ countdown.index +1 }}</span> <span ng-if="!countdown.description">Countdown {{ $index +1 }}</span>
<!-- remove countdown button --> <!-- remove countdown button -->
<button type="button" class="close" <button type="button" class="close"
ng-click="removeCountdown(countdown)" ng-click="removeCountdown(countdown)"
@ -109,36 +154,58 @@
</button> </button>
<!-- edit countdown button --> <!-- edit countdown button -->
<button type="button" class="close editicon" <button type="button" class="close editicon"
ng-click="editCountdownFlag=true;" ng-click="countdown.editFlag=true;"
title="{{ 'Edit countdown' | translate}}"> title="{{ 'Edit countdown' | translate}}">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</button> </button>
</div> </div>
<div class="panel-body" <div class="panel-body"
ng-class="{ 'projected': countdown.visible }"> ng-class="{ 'projected': isProjected(countdown).length }">
<!-- project countdown button --> <!-- project countdown button -->
<a class="btn btn-default btn-sm" <div class="btn-group" style="width:54px;" uib-dropdown>
ng-model="countdown.visible" <button type="button" class="btn btn-default btn-sm"
ng-click="showCountdown(countdown)" ng-click="project(countdown)"
ng-class="{ 'btn-primary': countdown.visible }" ng-class="{ 'btn-primary': isProjected(countdown).length }">
title="{{ 'Project countdown' | translate }}">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</button>
<button ng-if="projectors.length > 1" type="button" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-if="projectors.length > 1" style="text-align: center;">
<span class="pointer" ng-click="selectAll(countdown, true); preventClose($event)" translate>
All
</span>
| <span class="pointer" ng-click="selectAll(countdown, false); preventClose($event)" translate>
None
</span>
</li>
<li class="divider" ng-if="projectors.length > 1"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="toggleProjector(countdown, projector); preventClose($event)"
ng-class="{ 'projected': isProjectedOn(countdown, projector) }">
<i class="fa fa-square-o" ng-hide="hasProjector(countdown, projector)"></i>
<i class="fa fa-check-square-o" ng-show="hasProjector(countdown, projector)"></i>
{{ projector.name }}
</a> </a>
</li>
</ul>
</div>
&nbsp;&nbsp; &nbsp;&nbsp;
<!-- countdown controls --> <!-- countdown controls -->
<a class="btn btn-default vcenter" <a class="btn btn-default vcenter"
ng-click="resetCountdown(countdown)" ng-click="resetCountdown(countdown)"
ng-class="{ 'disabled': countdown.status == 'stop' && countdown.default == countdown.countdown_time }" ng-class="{ 'disabled': !countdown.running && countdown.default_time == countdown.countdown_time }"
title="{{ 'Reset countdown' | translate}}"> title="{{ 'Reset countdown' | translate}}">
<i class="fa fa-stop"></i> <i class="fa fa-stop"></i>
</a> </a>
<a ng-if="countdown.status=='stop'" class="btn btn-default vcenter" <a ng-if="!countdown.running" class="btn btn-default vcenter"
ng-click="startCountdown(countdown)" ng-click="startCountdown(countdown)"
title="{{ 'Start' | translate}}"> title="{{ 'Start' | translate}}">
<i class="fa fa-play"></i> <i class="fa fa-play"></i>
<i ng-if="countdown.status=='running'" class="fa fa-pause"></i> <i ng-if="countdown.running" class="fa fa-pause"></i>
</a> </a>
<a ng-if="countdown.status=='running'" class="btn btn-default vcenter" <a ng-if="countdown.running" class="btn btn-default vcenter"
ng-click="stopCountdown(countdown)" ng-click="stopCountdown(countdown)"
title="{{ 'Pause' | translate}}"> title="{{ 'Pause' | translate}}">
<i class="fa fa-pause"></i> <i class="fa fa-pause"></i>
@ -150,22 +217,29 @@
{{ countdown.seconds | osSecondsToTime }} {{ countdown.seconds | osSecondsToTime }}
</span> </span>
<!-- edit countdown form --> <!-- edit countdown form -->
<form ng-show="editCountdownFlag" ng-submit="editCountdown(countdown)"> <form ng-show="countdown.editFlag"
ng-submit="editCountdown(countdown)">
<div class="form-group"> <div class="form-group">
<label translate>Description</label> <label translate>Description</label>
<input ng-model="countdown.description" type="text" class="form-control input-sm"> <input ng-model="countdown.description" type="text" class="form-control input-sm">
</div> </div>
<div class="form-group"> <div class="form-group">
<label translate>Start time</label> <label translate>Start time</label>
<input data-ng-model="countdown.default" min-sec-format <div class="input-group">
<input data-ng-model="countdown.default_time" min-sec-format
type="text" placeholder="mm:ss" class="form-control input-sm"> type="text" placeholder="mm:ss" class="form-control input-sm">
<div class="input-group-addon pointer" uib-tooltip="{{ 'Reset countdown' | translate }}"
ng-click="countdown.reset()">
<i class="fa fa-undo"></i>
</div>
</div>
</div> </div>
<button type="submit" <button type="submit"
title="{{ 'Save' | translate}}" title="{{ 'Save' | translate}}"
class="btn btn-sm btn-primary"> class="btn btn-sm btn-primary">
<i class="fa fa-check"></i> <i class="fa fa-check"></i>
</button> </button>
<button ng-click="editCountdownFlag=false;" <button ng-click="countdown.editFlag=false;"
title="{{ 'Cancel' | translate}}" title="{{ 'Cancel' | translate}}"
class="btn btn-default btn-sm"> class="btn btn-default btn-sm">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
@ -182,7 +256,6 @@
</div> </div>
</div> </div>
<!-- messages --> <!-- messages -->
<div class="section" os-perms="core.can_manage_projector"> <div class="section" os-perms="core.can_manage_projector">
<a href="#" ng-click="isMessages = !isMessages"> <a href="#" ng-click="isMessages = !isMessages">
@ -190,10 +263,10 @@
<h4 translate>Messages</h4> <h4 translate>Messages</h4>
</a> </a>
<div uib-collapse="!isMessages" ng-cloak> <div uib-collapse="!isMessages" ng-cloak>
<div ng-repeat="message in messages | orderBy: 'index'" id="{{message.uuid}}" class="message panel panel-default"> <div ng-repeat="message in messages" id="message{{message.id}}" class="message panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span>{{ 'Message' | translate }} {{ message.index + 1 }}</span> <span>{{ 'Message' | translate }} {{ $index + 1 }}</span>
<!-- remove message button --> <!-- remove message button -->
<button type="button" class="close" <button type="button" class="close"
ng-click="removeMessage(message)" ng-click="removeMessage(message)"
@ -201,29 +274,52 @@
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
</button> </button>
<button type="button" class="close editicon" <button type="button" class="close editicon"
ng-click="editMessageFlag=true;" ng-click="message.editFlag=true"
title="{{ 'Edit message' | translate}}"> title="{{ 'Edit message' | translate}}">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</button> </button>
</div> </div>
<div class="panel-body" <div class="panel-body"
ng-class="{ 'projected': message.visible }"> ng-class="{ 'projected': isProjected(message).length }">
<div class="projectorbtn"> <div class="projectorbtn">
<!-- project message button --> <!-- project message button -->
<a class="btn btn-default btn-sm" <div class="btn-group" style="width:54px;" uib-dropdown>
ng-model="message.visible" <button type="button" class="btn btn-default btn-sm"
ng-click="showMessage(message)" ng-click="project(message)"
ng-class="{ 'btn-primary': message.visible }" ng-class="{ 'btn-primary': isProjected(message).length }">
title="{{ 'Project message' | translate }}" float="left">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</button>
<button type="button" ng-if="projectors.length > 1" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-if="projectors.length > 1" style="text-align: center;">
<span class="pointer" ng-click="selectAll(message, true); preventClose($event)" translate>
All
</span>
| <span class="pointer" ng-click="selectAll(message, false); preventClose($event)" translate>
None
</span>
</li>
<li class="divider" ng-if="projectors.length > 1"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="toggleProjector(message, projector); preventClose($event)"
ng-class="{ 'projected': isProjectedOn(message, projector) }">
<i class="fa fa-square-o" ng-hide="hasProjector(message, projector)"></i>
<i class="fa fa-check-square-o" ng-show="hasProjector(message, projector)"></i>
{{ projector.name }}
</a> </a>
</li>
</ul>
</div>
</div> </div>
&nbsp;&nbsp; &nbsp;&nbsp;
<div class="innermessage" ng-bind-html="message.message"> </div> <div class="innermessage" ng-bind-html="message.message"> </div>
<div class="panel-input"> <div class="panel-input">
<div ng-if="editMessageFlag" class="input-group"> <div ng-if="message.editFlag" class="input-group">
<input ng-model="message.message" type="text" class="form-control input-sm"> <input ng-model="message.message" type="text" class="form-control input-sm">
<a ng-click="editMessage(message)" <a ng-click="editMessage(message)"
title="{{ 'Save' | translate}}" title="{{ 'Save' | translate}}"

View File

@ -1,5 +1,5 @@
<div ng-controller="SlideCountdownCtrl"> <div ng-controller="SlideCountdownCtrl">
<div ng-if="visible"> <div ng-if="visible && selected">
<div class="countdown well pull-right" <div class="countdown well pull-right"
ng-class="{ ng-class="{
'negative': seconds <= 0, 'negative': seconds <= 0,

View File

@ -1,4 +1,4 @@
<div ng-controller="SlideMessageCtrl"> <div ng-controller="SlideMessageCtrl">
<div ng-if="visible" class="message_background"></div> <div ng-if="visible && selected" class="message_background"></div>
<div ng-if="visible" class="message well" ng-bind-html="message"></div> <div ng-if="visible && selected" class="message well" ng-class="{'identify': type=='identify'}" ng-bind-html="message"></div>
</div> </div>

View File

@ -0,0 +1,27 @@
<div class="btn-group" style="min-width:{{ projectors.length > 1 ? '54' : '34' }}px;" uib-dropdown
uib-tooltip="{{ 'Projector' | translate }} {{ model.isProjected(additionalId) }}"
tooltip-enable="model.isProjected(additionalId) > 0"
os-perms="core.can_manage_projector">
<button type="button" class="btn btn-default btn-sm"
ng-click="model.project(defaultProjectorId, additionalId)"
ng-class="{ 'btn-primary': model.isProjected(additionalId) == defaultProjectorId }">
<i class="fa fa-video-camera"></i>
{{ content }}
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-class="{ 'btn-primary': (model.isProjected(additionalId) > 0 && model.isProjected(additionalId) != defaultProjectorId) }"
ng-if="projectors.length > 1"
uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="model.project(projector.id, additionalId)"
ng-class="{ 'projected': (model.isProjected(additionalId) == projector.id) }">
<i class="fa fa-video-camera" ng-show="model.isProjected(additionalId) == projector.id"></i>
{{ projector.name }}
<span ng-if="defaultProjectorId == projector.id">(<translate>Default</translate>)</span>
</a>
</li>
</ul>
</div>

View File

@ -37,10 +37,14 @@
} }
</style> </style>
<div id="iframewrapper"> <div id="iframewrapper" ng-hide="error">
<iframe id="iframe" src="/real-projector" frameborder="0"></iframe> <iframe id="iframe" ng-src="{{ '/real-projector/' + projector_id }}" frameborder="0"></iframe>
<div id="iframeoverlay"></div> <div id="iframeoverlay"></div>
</div> </div>
<div class="error" ng-show="error">
<p>{{ error | translate }}</p>
</div>
</div> </div>
</div> </div>

View File

@ -11,20 +11,28 @@
<script src="static/js/openslides.js"></script> <script src="static/js/openslides.js"></script>
<script src="static/js/openslides-templates.js"></script> <script src="static/js/openslides-templates.js"></script>
<style type="text/css"> <div id="projectorContainer" ng-controller="ProjectorCtrl">
<style type="text/css">
#header, #footer { #header, #footer {
background-color: {{ config('projector_header_backgroundcolor') }}; background-color: {{ config('projector_header_backgroundcolor') }};
} }
#header, #footer, #currentTime { #header, #footer, #currentTime {
color: {{ config('projector_header_fontcolor') }}; color: {{ config('projector_header_fontcolor') }};
} }
#header, #footer, .contentContainer {
visibility: {{ blank ? 'hidden' : 'visible' }};
}
#projectorContainer {
background-color: {{ blank ? config('projector_blank_color') : '#fff' }};
height: {{ blank ? '100%' : 'auto' }};
}
h1 { h1 {
color: {{ config('projector_h1_fontcolor') }}; color: {{ config('projector_h1_fontcolor') }};
} }
</style> </style>
<div id="header"> <div id="header">
<img ng-if="config('projector_enable_logo')" id="logo" src="/static/img/logo-projector.png" alt="OpenSlides" /> <img ng-if="config('projector_enable_logo')" id="logo" src="/static/img/logo-projector.png" alt="OpenSlides" />
<div ng-if="config('projector_enable_title')" id="eventdata"> <div ng-if="config('projector_enable_title')" id="eventdata">
<div class="title" ng-class="{ 'titleonly': !config('general_event_description') }" <div class="title" ng-class="{ 'titleonly': !config('general_event_description') }"
@ -32,13 +40,12 @@
<div ng-if="config('general_event_description')" class="description" <div ng-if="config('general_event_description')" class="description"
ng-bind-html="config('general_event_description')"></div> ng-bind-html="config('general_event_description')"></div>
</div> </div>
</div> </div>
<div ng-controller="ProjectorCtrl">
<style type="text/css"> <style type="text/css">
.scrollcontent { .scrollcontent {
margin-top: {{scroll}}px !important; margin-top: {{ -80 * projector.scroll }}px !important;
font-size: {{scale}}%; font-size: {{ 100 + 20 * projector.scale }}%;
} }
.mediascrollcontent { .mediascrollcontent {
margin-top: {{scroll/2}}em !important; margin-top: {{scroll/2}}em !important;
@ -47,12 +54,11 @@
transform: scale({{scale/100}}); transform: scale({{scale/100}});
} }
</style> </style>
<div ng-repeat="element in elements | orderBy:'index'"> <div class="contentContainer" ng-repeat="element in elements | orderBy:'index'">
<div ng-include="element.template"></div> <div ng-include="element.template"></div>
</div> </div>
</div>
<div id="footer"> <div id="footer">
<span ng-if="config('general_event_date')"> <span ng-if="config('general_event_date')">
{{ config('general_event_date') }} {{ config('general_event_date') }}
</span> </span>
@ -62,6 +68,7 @@
<span ng-if="config('general_event_location')"> <span ng-if="config('general_event_location')">
{{ config('general_event_location') }} {{ config('general_event_location') }}
</span> </span>
</div>
</div> </div>
<script src="/webclient/projector/"></script> <script src="/webclient/projector/"></script>

View File

@ -28,10 +28,10 @@ urlpatterns = [
name='core_webclient_javascript'), name='core_webclient_javascript'),
# View for the projectors are handled by angular. # View for the projectors are handled by angular.
url(r'^projector.*$', views.ProjectorView.as_view()), url(r'^projector/(\d+)/$', views.ProjectorView.as_view()),
# Original view without resolutioncontrol for the projectors are handled by angular. # Original view without resolutioncontrol for the projectors are handled by angular.
url(r'^real-projector.*$', views.RealProjectorView.as_view()), url(r'^real-projector/(\d+)/$', views.RealProjectorView.as_view()),
# Main entry point for all angular pages. # Main entry point for all angular pages.
# Has to be the last entry in the urls.py # Has to be the last entry in the urls.py

View File

@ -25,7 +25,6 @@ from openslides.utils.plugins import (
) )
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
ModelViewSet, ModelViewSet,
ReadOnlyModelViewSet,
Response, Response,
SimpleMetadata, SimpleMetadata,
ValidationError, ValidationError,
@ -42,7 +41,7 @@ from .access_permissions import (
) )
from .config import config from .config import config
from .exceptions import ConfigError, ConfigNotFound from .exceptions import ConfigError, ConfigNotFound
from .models import ChatMessage, Projector, Tag from .models import ChatMessage, ProjectionDefault, Projector, Tag
# Special Django views # Special Django views
@ -171,7 +170,7 @@ class WebclientJavaScriptView(utils_views.View):
# Viewsets for the REST API # Viewsets for the REST API
class ProjectorViewSet(ReadOnlyModelViewSet): class ProjectorViewSet(ModelViewSet):
""" """
API endpoint for the projector slide info. API endpoint for the projector slide info.
@ -190,15 +189,27 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
result = self.get_access_permissions().check_permissions(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action == 'metadata': elif self.action == 'metadata':
result = self.request.user.has_perm('core.can_see_projector') result = self.request.user.has_perm('core.can_see_projector')
elif self.action in ('activate_elements', 'prune_elements', 'update_elements', elif self.action in (
'deactivate_elements', 'clear_elements', 'control_view', 'create', 'update', 'partial_update', 'destroy',
'set_resolution', 'set_scroll'): 'activate_elements', 'prune_elements', 'update_elements', 'deactivate_elements', 'clear_elements',
'control_view', 'set_resolution', 'set_scroll', 'control_blank', 'broadcast',
'set_projectiondefault',
):
result = (self.request.user.has_perm('core.can_see_projector') and result = (self.request.user.has_perm('core.can_see_projector') and
self.request.user.has_perm('core.can_manage_projector')) self.request.user.has_perm('core.can_manage_projector'))
else: else:
result = False result = False
return result return result
# Assign all ProjectionDefault objects from this projector to the default projector (pk=1).
def destroy(self, *args, **kwargs):
projector_instance = self.get_object()
for projection_default in ProjectionDefault.objects.all():
if projection_default.projector.id == projector_instance.id:
projection_default.projector_id = 1
projection_default.save()
return super(ProjectorViewSet, self).destroy(*args, **kwargs)
@detail_route(methods=['post']) @detail_route(methods=['post'])
def activate_elements(self, request, pk): def activate_elements(self, request, pk):
""" """
@ -447,6 +458,68 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
scroll=request.data) scroll=request.data)
return Response({'detail': message}) return Response({'detail': message})
@detail_route(methods=['post'])
def control_blank(self, request, pk):
"""
REST API operation to blank the projector.
It expects a POST request to
/rest/core/projector/<pk>/control_blank/ with a value for blank.
"""
if not isinstance(request.data, bool):
raise ValidationError({'detail': 'Data must be a bool.'})
projector_instance = self.get_object()
projector_instance.blank = request.data
projector_instance.save()
message = "Setting 'blank' to {blank} was successful.".format(
blank=request.data)
return Response({'detail': message})
@detail_route(methods=['post'])
def broadcast(self, request, pk):
"""
REST API operation to (un-)broadcast the given projector.
This method takes care, that all other projectors get the new requirements.
It expects a POST request to
/rest/core/projector/<pk>/broadcast/ without an argument
"""
if config['projector_broadcast'] == 0:
config['projector_broadcast'] = pk
message = "Setting projector {id} as broadcast projector was successful.".format(
id=pk)
else:
config['projector_broadcast'] = 0
message = "Disabling broadcast was successful."
return Response({'detail': message})
@detail_route(methods=['post'])
def set_projectiondefault(self, request, pk):
"""
REST API operation to set a projectiondefault to the requested projector. The argument
has to be an int representing the pk from the projectiondefault to be set.
It expects a POST request to
/rest/core/projector/<pk>/set_projectiondefault/ with the projectiondefault id as the argument
"""
if not isinstance(request.data, int):
raise ValidationError({'detail': 'Data must be an int.'})
try:
projectiondefault = ProjectionDefault.objects.get(pk=request.data)
except ProjectionDefault.DoesNotExist:
raise ValidationError({'detail': 'The projectiondefault with pk={pk} was not found.'.format(
pk=request.data)})
else:
projector_instance = self.get_object()
projectiondefault.projector = projector_instance
projectiondefault.save()
return Response('Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format(
name=projectiondefault.display_name,
projector_id=projector_instance.pk))
class TagViewSet(ModelViewSet): class TagViewSet(ModelViewSet):
""" """

View File

@ -56,20 +56,36 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
'MediafileForm', 'MediafileForm',
'User', 'User',
'Projector', 'Projector',
function($scope, $http, ngDialog, Mediafile, MediafileForm, User, Projector) { 'ProjectionDefault',
function($scope, $http, ngDialog, Mediafile, MediafileForm, User, Projector, ProjectionDefault) {
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
$scope.$watch(function() {
return Projector.lastModified();
}, function() {
$scope.projectors = Projector.getAll();
updatePresentedMediafiles();
});
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'mediafiles'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
// setup table sorting function updatePresentedMediafiles () {
$scope.sortColumn = 'title'; $scope.presentedMediafiles = [];
$scope.filterPresent = ''; Projector.getAll().forEach(function (projector) {
$scope.reverse = false; var projectorElements = _.map(projector.elements, function(element) { return element; });
var mediaElements = _.filter(projectorElements, function (element) {
function updatePresentedMediafiles() {
var projectorElements = _.map(Projector.get(1).elements, function(element) { return element; });
$scope.presentedMediafiles = _.filter(projectorElements, function (element) {
return element.name === 'mediafiles/mediafile'; return element.name === 'mediafiles/mediafile';
}); });
mediaElements.forEach(function (element) {
$scope.presentedMediafiles.push(element);
});
});
if ($scope.presentedMediafiles.length) { if ($scope.presentedMediafiles.length) {
$scope.isMeta = false; $scope.isMeta = false;
} else { } else {
@ -77,12 +93,13 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
} }
} }
$scope.$watch(function() {
return Projector.get(1).elements;
}, updatePresentedMediafiles);
updatePresentedMediafiles(); updatePresentedMediafiles();
// setup table sorting
$scope.sortColumn = 'title';
$scope.filterPresent = '';
$scope.reverse = false;
// function to sort by clicked column // function to sort by clicked column
$scope.toggleSort = function ( column ) { $scope.toggleSort = function ( column ) {
if ( $scope.sortColumn === column ) { if ( $scope.sortColumn === column ) {
@ -138,8 +155,13 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
// ** PDF presentation functions **/ // ** PDF presentation functions **/
// show document on projector // show document on projector
$scope.showMediafile = function (mediafile) { $scope.showMediafile = function (projectorId, mediafile) {
var postUrl = '/rest/core/projector/1/prune_elements/'; var isProjectedId = mediafile.isProjected();
if (isProjectedId > 0) {
$http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
if (isProjectedId != projectorId) {
var postUrl = '/rest/core/projector/' + projectorId + '/prune_elements/';
var data = [{ var data = [{
name: 'mediafiles/mediafile', name: 'mediafiles/mediafile',
id: mediafile.id, id: mediafile.id,
@ -152,23 +174,25 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
fullscreen: mediafile.is_pdf fullscreen: mediafile.is_pdf
}]; }];
$http.post(postUrl, data); $http.post(postUrl, data);
}
}; };
function sendMediafileCommand(data) { var sendMediafileCommand = function (mediafile, data) {
var mediafileElement = getCurrentlyPresentedMediafile(); var updateData = _.extend({}, mediafile);
var updateData = _.extend({}, mediafileElement);
_.extend(updateData, data); _.extend(updateData, data);
var postData = {}; var postData = {};
postData[mediafileElement.uuid] = updateData; postData[mediafile.uuid] = updateData;
$http.post('/rest/core/projector/1/update_elements/', postData);
}
function getCurrentlyPresentedMediafile() { // Find Projector where the mediafile is projected
return $scope.presentedMediafiles[0]; $scope.projectors.forEach(function (projector) {
if (_.find(projector.elements, function (e) {return e.uuid == mediafile.uuid;})) {
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', postData);
} }
});
};
$scope.getTitle = function (presentedMediafile) { $scope.getTitle = function (mediafile) {
return Mediafile.get(presentedMediafile.id).title; return Mediafile.get(mediafile.id).title;
}; };
$scope.getType = function(presentedMediafile) { $scope.getType = function(presentedMediafile) {
@ -176,75 +200,69 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
return mediafile.is_pdf ? 'pdf' : mediafile.is_image ? 'image' : 'video'; return mediafile.is_pdf ? 'pdf' : mediafile.is_image ? 'image' : 'video';
}; };
$scope.mediafileGoToPage = function (page) { $scope.mediafileGoToPage = function (mediafile, page) {
var mediafileElement = getCurrentlyPresentedMediafile();
if (parseInt(page) > 0) { if (parseInt(page) > 0) {
sendMediafileCommand({ sendMediafileCommand(
page: parseInt(page) mediafile,
}); {page: parseInt(page)}
);
} }
}; };
$scope.mediafileZoomIn = function () { $scope.mediafileZoomIn = function (mediafile) {
var mediafileElement = getCurrentlyPresentedMediafile();
var scale = 1; var scale = 1;
if (parseFloat(mediafileElement.scale)) { if (parseFloat(mediafile.scale)) {
scale = mediafileElement.scale; scale = mediafile.scale;
} }
sendMediafileCommand({ sendMediafileCommand(
scale: scale + 0.2 mediafile,
}); {scale: scale + 0.2}
);
}; };
$scope.mediafileFit = function () { $scope.mediafileFit = function (mediafile) {
sendMediafileCommand({ sendMediafileCommand(
scale: 'page-fit' mediafile,
}); {scale: 'page-fit'}
);
}; };
$scope.mediafileZoomOut = function () { $scope.mediafileZoomOut = function (mediafile) {
var mediafileElement = getCurrentlyPresentedMediafile();
var scale = 1; var scale = 1;
if (parseFloat(mediafileElement.scale)) { if (parseFloat(mediafile.scale)) {
scale = mediafileElement.scale; scale = mediafile.scale;
} }
sendMediafileCommand({ sendMediafileCommand(
scale: scale - 0.2 mediafile,
}); {scale: scale - 0.2}
);
}; };
$scope.mediafileChangePage = function(pageNum) { $scope.mediafileChangePage = function(mediafile, pageNum) {
sendMediafileCommand({ sendMediafileCommand(
pageToDisplay: pageNum mediafile,
}); {pageToDisplay: pageNum}
);
}; };
$scope.mediafileRotate = function () { $scope.mediafileRotate = function (mediafile) {
var mediafileElement = getCurrentlyPresentedMediafile(); var rotation = mediafile.rotate;
var rotation = mediafileElement.rotate;
if (rotation === 270) { if (rotation === 270) {
rotation = 0; rotation = 0;
} else { } else {
rotation = rotation + 90; rotation = rotation + 90;
} }
sendMediafileCommand({ sendMediafileCommand(
rotate: rotation mediafile,
}); {rotate: rotation}
);
}; };
$scope.mediafileScroll = function(scroll) { $scope.mediafileToggleFullscreen = function(mediafile) {
var mediafileElement = getCurrentlyPresentedMediafile(); sendMediafileCommand(
sendMediafileCommand({ mediafile,
scroll: scroll {fullscreen: !mediafile.fullscreen}
}); );
}; };
var setFullscreen = function(fullscreen) { $scope.mediafileTogglePlaying = function(mediafile) {
sendMediafileCommand({ sendMediafileCommand(
fullscreen: fullscreen mediafile,
}); {playing: !mediafile.playing}
}; );
$scope.mediafileToggleFullscreen = function() {
var mediafileElement = getCurrentlyPresentedMediafile();
setFullscreen(!mediafileElement.fullscreen);
};
$scope.setPlaying = function(playing) {
sendMediafileCommand({
playing: playing
});
}; };
} }
]) ])

View File

@ -26,14 +26,15 @@
<div class="col-md-12"> <div class="col-md-12">
<div ng-repeat="presentedMediafile in presentedMediafiles"> <div ng-repeat="presentedMediafile in presentedMediafiles">
<h3>{{ getTitle(presentedMediafile) }}</h3> <h3>{{ getTitle(presentedMediafile) }}</h3>
<!-- PDF -->
<nav ng-show="getType(presentedMediafile) === 'pdf'" ng-class="getNavStyle(scroll)" class="form-inline"> <nav ng-show="getType(presentedMediafile) === 'pdf'" ng-class="getNavStyle(scroll)" class="form-inline">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default" ng-click="mediafileGoToPage(presentedMediafile.page - 1)" <button class="btn btn-default" ng-click="mediafileGoToPage(presentedMediafile, presentedMediafile.page - 1)"
ng-class="{ 'disabled': (presentedMediafile.page - 1) < 1 }" ng-class="{ 'disabled': (presentedMediafile.page - 1) < 1 }"
title="{{ 'Previous page' | translate }}"> title="{{ 'Previous page' | translate }}">
<i class="fa fa-backward"></i> <i class="fa fa-backward"></i>
</button> </button>
<button class="btn btn-default" ng-click="mediafileGoToPage(presentedMediafile.page + 1)" <button class="btn btn-default" ng-click="mediafileGoToPage(presentedMediafile, presentedMediafile.page + 1)"
ng-class="{ 'disabled': (presentedMediafile.page + 1) > presentedMediafile.numPages }" ng-class="{ 'disabled': (presentedMediafile.page + 1) > presentedMediafile.numPages }"
title="{{ 'Next page' | translate }}"> title="{{ 'Next page' | translate }}">
<i class="fa fa-forward"></i> <i class="fa fa-forward"></i>
@ -42,57 +43,62 @@
<div class="input-group"> <div class="input-group">
<span class="input-group-addon" translate>Page</span> <span class="input-group-addon" translate>Page</span>
<input type="number" min=1 ng-model="presentedMediafile.page" class="form-control" style="width: 80px" <input type="number" min=1 ng-model="presentedMediafile.page" class="form-control" style="width: 80px"
ng-change="mediafileGoToPage(presentedMediafile.page)"> ng-change="mediafileGoToPage(presentedMediafile, presentedMediafile.page)">
<span class="input-group-addon"><translate>of</translate> {{presentedMediafile.numPages}}</span> <span class="input-group-addon"><translate>of</translate> {{presentedMediafile.numPages}}</span>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default" ng-click="mediafileRotate()" title="{{ 'Rotate clockwise' | translate }}"> <button class="btn btn-default" ng-click="mediafileRotate(presentedMediafile)"
title="{{ 'Rotate clockwise' | translate }}">
<i class="fa fa-repeat"></i> <i class="fa fa-repeat"></i>
</button> </button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default" ng-click="mediafileZoomOut()" title="{{ 'Zoom out' | translate }}"> <button class="btn btn-default" ng-click="mediafileZoomOut(presentedMediafile)"
title="{{ 'Zoom out' | translate }}">
<i class="fa fa-search-minus"></i> <i class="fa fa-search-minus"></i>
</button> </button>
<button class="btn" ng-click="mediafileFit()" title="{{ 'Reset zoom' | translate }}" <button class="btn" ng-click="mediafileFit(presentedMediafile)"
title="{{ 'Reset zoom' | translate }}"
ng-class="presentedMediafile.scale=='page-fit' ? 'btn-primary' : 'btn-default'"> ng-class="presentedMediafile.scale=='page-fit' ? 'btn-primary' : 'btn-default'">
<i class="fa fa-arrows-alt"></i> <i class="fa fa-arrows-alt"></i>
</button> </button>
<button class="btn btn-default" ng-click="mediafileZoomIn()" title="{{ 'Zoom in' | translate }}"> <button class="btn btn-default" ng-click="mediafileZoomIn(presentedMediafile)"
title="{{ 'Zoom in' | translate }}">
<i class="fa fa-search-plus"></i> <i class="fa fa-search-plus"></i>
</button> </button>
</div> </div>
</nav> </nav>
<!-- Image -->
<nav ng-show="getType(presentedMediafile) === 'image'" ng-class="getNavStyle(scroll)" class="form-inline"> <nav ng-show="getType(presentedMediafile) === 'image'" ng-class="getNavStyle(scroll)" class="form-inline">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default" ng-click="mediafileToggleFullscreen()" title="{{ 'Toggle fullscreen' | translate }}" <button class="btn btn-default" ng-click="mediafileToggleFullscreen(presentedMediafile)"
title="{{ 'Toggle fullscreen' | translate }}"
ng-class="presentedMediafile.fullscreen ? 'btn-primary' : 'btn-default'"> ng-class="presentedMediafile.fullscreen ? 'btn-primary' : 'btn-default'">
<i class="fa fa-arrows-alt"></i> <i class="fa fa-arrows-alt"></i>
</button> </button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default" ng-click="mediafileRotate()" title="{{ 'Rotate clockwise' | translate }}"> <button class="btn btn-default" ng-click="mediafileRotate(presentedMediafile)"
title="{{ 'Rotate clockwise' | translate }}">
<i class="fa fa-repeat"></i> <i class="fa fa-repeat"></i>
</button> </button>
</div> </div>
</nav> </nav>
<!-- Video -->
<nav ng-show="getType(presentedMediafile) === 'video'" ng-class="getNavStyle(scroll)" class="form-inline"> <nav ng-show="getType(presentedMediafile) === 'video'" ng-class="getNavStyle(scroll)" class="form-inline">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default" ng-click="mediafileToggleFullscreen()" title="{{ 'Toggle fullscreen' | translate }}" <button class="btn btn-default" ng-click="mediafileToggleFullscreen(presentedMediafile)"
title="{{ 'Toggle fullscreen' | translate }}"
ng-class="presentedMediafile.fullscreen ? 'btn-primary' : 'btn-default'"> ng-class="presentedMediafile.fullscreen ? 'btn-primary' : 'btn-default'">
<i class="fa fa-arrows-alt"></i> <i class="fa fa-arrows-alt"></i>
</button> </button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default" ng-click="setPlaying(false)" title="{{ 'Stop' | translate }}" <button class="btn btn-default" ng-click="mediafileTogglePlaying(presentedMediafile)"
ng-class="presentedMediafile.playing ? 'btn-default' : 'btn-primary'"> title="{{ 'Start/stop video' | translate }}">
<i class="fa fa-stop"></i> <i class="fa" ng-class="presentedMediafile.playing ? 'fa-stop' : 'fa-play'"></i>
</button>
</div>
<div class="btn-group">
<button class="btn btn-default" ng-click="setPlaying(true)" title="{{ 'Play' | translate }}"
ng-class="presentedMediafile.playing ? 'btn-primary' : 'btn-default'">
<i class="fa fa-play"></i>
</button> </button>
</div> </div>
</nav> </nav>
@ -195,13 +201,32 @@
<!-- projector column --> <!-- projector column -->
<td ng-show="!isDeleteMode" <td ng-show="!isDeleteMode"
os-perms="core.can_manage_projector"> os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm" <div class="btn-group" style="min-width:54px;" uib-dropdown
ng-if="mediafile.is_presentable" ng-if="mediafile.is_presentable"
ng-class="{ 'btn-primary': mediafile.isProjected() }" uib-tooltip="{{ 'Projektor' | translate }} {{ mediafile.isProjected() }}"
ng-click="showMediafile(mediafile)" tooltip-enable="mediafile.isProjected() > 0">
title="{{ 'Project mediafile' | translate }}"> <button type="button" class="btn btn-default btn-sm"
ng-click="showMediafile(defaultProjectorId, mediafile)"
ng-class="{ 'btn-primary': mediafile.isProjected() == defaultProjectorId }">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-class="{ 'btn-primary': (mediafile.isProjected() > 0 && mediafile.isProjected() != defaultProjectorId) }"
ng-if="projectors.length > 1"
uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="showMediafile(projector.id, mediafile)"
ng-class="{ 'projected': (mediafile.isProjected() == projector.id) }">
<i class="fa fa-video-camera" ng-show="mediafile.isProjected() == projector.id"></i>
{{ projector.name }}
<span ng-if="defaultProjectorId == projector.id">(<translate>Standard</translate>)</span>
</a> </a>
</li>
</ul>
</div>
<!-- delete selection column --> <!-- delete selection column -->
<td ng-show="isDeleteMode" os-perms="mediafiles.can_manage" class="deleteColumn"> <td ng-show="isDeleteMode" os-perms="mediafiles.can_manage" class="deleteColumn">
<input type="checkbox" ng-model="mediafile.selected"> <input type="checkbox" ng-model="mediafile.selected">

View File

@ -21,7 +21,8 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
'User', 'User',
'Config', 'Config',
'Projector', 'Projector',
function($scope, $rootScope, $http, Motion, User, Config, Projector) { '$timeout',
function($scope, $rootScope, $http, Motion, User, Config, Projector, $timeout) {
// Attention! Each object that is used here has to be dealt on server side. // 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 // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
@ -80,6 +81,7 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
Motion.bindOne(id, $scope, 'motion'); Motion.bindOne(id, $scope, 'motion');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
} }
]); ]);

View File

@ -917,14 +917,25 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'PdfMakeDocumentProvider', 'PdfMakeDocumentProvider',
'gettextCatalog', 'gettextCatalog',
'HTMLValidizer', 'HTMLValidizer',
'Projector',
'ProjectionDefault',
function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda, MotionDocxExport, function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda, MotionDocxExport,
MotionContentProvider, MotionCatalogContentProvider, PdfMakeConverter, MotionContentProvider, MotionCatalogContentProvider, PdfMakeConverter, PdfMakeDocumentProvider,
PdfMakeDocumentProvider, gettextCatalog, HTMLValidizer) { gettextCatalog, HTMLValidizer, Projector, ProjectionDefault) {
Motion.bindAll({}, $scope, 'motions'); Motion.bindAll({}, $scope, 'motions');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Tag.bindAll({}, $scope, 'tags'); Tag.bindAll({}, $scope, 'tags');
Workflow.bindAll({}, $scope, 'workflows'); Workflow.bindAll({}, $scope, 'workflows');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Projector.bindAll({}, $scope, 'projectors');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'motions'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.alert = {}; $scope.alert = {};
// setup table sorting // setup table sorting
@ -1192,9 +1203,10 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'gettextCatalog', 'gettextCatalog',
'Projector', 'Projector',
'HTMLValidizer', 'HTMLValidizer',
'ProjectionDefault',
function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Config, function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Config,
motion, MotionContentProvider, PollContentProvider, motion, MotionContentProvider, PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider,
PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog, Projector, HTMLValidizer) { MotionInlineEditing, gettextCatalog, Projector, HTMLValidizer, ProjectionDefault) {
Motion.bindOne(motion.id, $scope, 'motion'); Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
@ -1202,6 +1214,11 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Workflow.bindAll({}, $scope, 'workflows'); Workflow.bindAll({}, $scope, 'workflows');
Motion.loadRelations(motion, 'agenda_item'); Motion.loadRelations(motion, 'agenda_item');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
$scope.defaultProjectorId = ProjectionDefault.filter({name: 'motions'})[0].projector_id;
});
$scope.version = motion.active_version; $scope.version = motion.active_version;
$scope.isCollapsed = true; $scope.isCollapsed = true;
$scope.commentsFields = Config.get('motions_comments').value; $scope.commentsFields = Config.get('motions_comments').value;

View File

@ -16,12 +16,8 @@
<translate>List of speakers</translate> <translate>List of speakers</translate>
</a> </a>
<!-- project --> <!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <projector-button model="motion" default-projector-id="defaultProjectorId">
ng-class="{ 'btn-primary': motion.isProjected() }" </projector-button>
ng-click="motion.project()"
title="{{ 'Project motion' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<!-- edit --> <!-- edit -->
<a ng-if="motion.isAllowed('update')" ng-click="openDialog(motion)" <a ng-if="motion.isAllowed('update')" ng-click="openDialog(motion)"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"

View File

@ -98,7 +98,6 @@
ng-click="checkAll()"></i> ng-click="checkAll()"></i>
</div> </div>
<div class="col-xs-11 main-header"> <div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right"> <span class="form-inline text-right pull-right">
<span class="sort-spacer pointer" ng-click="reset_filters()" <span class="sort-spacer pointer" ng-click="reset_filters()"
ng-if="are_filters_set()" ng-disabled="isDeleteMode" ng-if="are_filters_set()" ng-disabled="isDeleteMode"
@ -304,12 +303,8 @@
</div> </div>
<!-- projector column --> <!-- projector column -->
<div class="col-xs-1 centered" os-perms="core.can_manage_projector"> <div class="col-xs-1 centered" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm" <projector-button model="motion", default-projector-id="defaultProjectorId">
ng-class="{ 'btn-primary': motion.isProjected() }" </projector-button>
ng-click="motion.project()"
title="{{ 'Project motion' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
</div> </div>
<!-- main content column --> <!-- main content column -->
<div class="col-xs-6 content"> <div class="col-xs-6 content">

View File

@ -3,6 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import openslides.utils.models import openslides.utils.models

View File

@ -156,8 +156,18 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics'])
'TopicForm', 'TopicForm',
'Topic', 'Topic',
'topic', 'topic',
function($scope, ngDialog, TopicForm, Topic, topic) { 'Projector',
'ProjectionDefault',
function($scope, ngDialog, TopicForm, Topic, topic, Projector, ProjectionDefault) {
Topic.bindOne(topic.id, $scope, 'topic'); Topic.bindOne(topic.id, $scope, 'topic');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'topics'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
Topic.loadRelations(topic, 'agenda_item'); Topic.loadRelations(topic, 'agenda_item');
$scope.openDialog = function (topic) { $scope.openDialog = function (topic) {
ngDialog.open(TopicForm.getDialog(topic)); ngDialog.open(TopicForm.getDialog(topic));

View File

@ -11,12 +11,8 @@
<translate>List of speakers</translate> <translate>List of speakers</translate>
</a> </a>
<!-- project --> <!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <projector-button model="topic" default-projector-id="defaultProjectorId">
ng-class="{ 'btn-primary': topic.isProjected() }" </projector-button>
ng-click="topic.project()"
title="{{ 'Project topic' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<!-- edit --> <!-- edit -->
<a os-perms="agenda.can_manage" ng-click="openDialog(topic)" <a os-perms="agenda.can_manage" ng-click="openDialog(topic)"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"

View File

@ -421,9 +421,19 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
'User', 'User',
'Group', 'Group',
'PasswordGenerator', 'PasswordGenerator',
function($scope, $state, $http, ngDialog, UserForm, User, Group, PasswordGenerator) { 'Projector',
'ProjectionDefault',
function($scope, $state, $http, ngDialog, UserForm, User, Group, PasswordGenerator, Projector, ProjectionDefault) {
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Group.bindAll({where: {id: {'>': 1}}}, $scope, 'groups'); Group.bindAll({where: {id: {'>': 1}}}, $scope, 'groups');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'users'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.alert = {}; $scope.alert = {};
$scope.groupFilter = undefined; $scope.groupFilter = undefined;

View File

@ -184,12 +184,8 @@
ng-class="{ 'activeline': user.isProjected(), 'selected': user.selected }"> ng-class="{ 'activeline': user.isProjected(), 'selected': user.selected }">
<!-- projector column --> <!-- projector column -->
<td ng-show="!isSelectMode" os-perms="core.can_manage_projector"> <td ng-show="!isSelectMode" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm" <projector-button model="user" default-projector-id="defaultProjectorId">
ng-class="{ 'btn-primary': user.isProjected() }" </projector-button>
ng-click="user.project()"
title="{{ 'Project user' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<!-- selection column --> <!-- selection column -->
<td ng-show="isSelectMode" os-perms="users.can_manage" class="deleteColumn"> <td ng-show="isSelectMode" os-perms="users.can_manage" class="deleteColumn">
<input type="checkbox" ng-model="user.selected"> <input type="checkbox" ng-model="user.selected">

View File

@ -64,7 +64,7 @@ def ws_add_projector(message, projector_id):
""" """
user = message.user user = message.user
# user is the django anonymous user. We have our own. # user is the django anonymous user. We have our own.
if user.is_anonymous: if user.is_anonymous and config['general_systen_enable_anonymous']:
user = AnonymousUser() user = AnonymousUser()
if not user.has_perm('core.can_see_projector'): if not user.has_perm('core.can_see_projector'):
@ -138,11 +138,24 @@ def send_data(message):
projectors = Projector.get_projectors_that_show_this(collection_element) projectors = Projector.get_projectors_that_show_this(collection_element)
send_all = None # The decission is done later send_all = None # The decission is done later
broadcast_id = config['projector_broadcast']
if broadcast_id > 0:
projectors = Projector.objects.all() # Also the broadcasted projector should get his data
send_all = True
broadcast_projector_data = get_projector_element_data(Projector.objects.get(pk=broadcast_id))
broadcast_projector_data.append(CollectionElement.from_values(
collection_string=Projector.get_collection_string(), id=broadcast_id).as_autoupdate_for_projector())
else:
broadcast_projector_data = None
for projector in projectors: for projector in projectors:
if send_all is None: if send_all is None:
send_all = projector.need_full_update_for_this(collection_element) send_all = projector.need_full_update_for_this(collection_element)
if send_all: if send_all:
if broadcast_projector_data is None:
output = get_projector_element_data(projector) output = get_projector_element_data(projector)
else:
output = broadcast_projector_data
else: else:
output = [] output = []
output.append(collection_element.as_autoupdate_for_projector()) output.append(collection_element.as_autoupdate_for_projector())

View File

@ -29,8 +29,6 @@ from rest_framework.serializers import ( # noqa
) )
from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
from rest_framework.viewsets import \
ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
from rest_framework.viewsets import ViewSet as _ViewSet # noqa from rest_framework.viewsets import ViewSet as _ViewSet # noqa
router = DefaultRouter() router = DefaultRouter()
@ -174,9 +172,5 @@ class ModelViewSet(PermissionMixin, _ModelViewSet):
pass pass
class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):
pass
class ViewSet(PermissionMixin, _ViewSet): class ViewSet(PermissionMixin, _ViewSet):
pass pass

View File

@ -216,17 +216,6 @@ class Speak(TestCase):
def test_begin_speech_with_countdown(self): def test_begin_speech_with_countdown(self):
config['agenda_couple_countdown_and_speakers'] = True 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.objects.add(self.user, self.item)
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item) speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
self.client.put( self.client.put(
@ -234,32 +223,22 @@ class Speak(TestCase):
{'speaker': speaker.pk}) {'speaker': speaker.pk})
for key, value in Projector.objects.get().config.items(): for key, value in Projector.objects.get().config.items():
if value['name'] == 'core/countdown': if value['name'] == 'core/countdown':
self.assertEqual(value['status'], 'running') self.assertTrue(value['running'])
success = True # If created, the countdown should have index 1
created = value['index'] == 1
break break
else: else:
success = False created = False
self.assertTrue(success) self.assertTrue(created)
def test_end_speech_with_countdown(self): def test_end_speech_with_countdown(self):
config['agenda_couple_countdown_and_speakers'] = True 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 = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
speaker.begin_speech() speaker.begin_speech()
self.client.delete(reverse('item-speak', args=[self.item.pk])) self.client.delete(reverse('item-speak', args=[self.item.pk]))
for key, value in Projector.objects.get().config.items(): for key, value in Projector.objects.get().config.items():
if value['name'] == 'core/countdown': if value['name'] == 'core/countdown':
self.assertEqual(value['status'], 'stop') self.assertFalse(value['running'])
success = True success = True
break break
else: else:

View File

@ -25,19 +25,13 @@ class ProjectorAPI(TestCase):
default_projector.save() default_projector.save()
response = self.client.get(reverse('projector-detail', args=['1'])) response = self.client.get(reverse('projector-detail', args=['1']))
content = json.loads(response.content.decode())
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode()), { self.assertEqual(content['elements'], {
'id': 1,
'elements': {
'aae4a07b26534cfb9af4232f361dce73': 'aae4a07b26534cfb9af4232f361dce73':
{'id': topic.id, {'id': topic.id,
'uuid': 'aae4a07b26534cfb9af4232f361dce73', 'uuid': 'aae4a07b26534cfb9af4232f361dce73',
'name': 'topics/topic'}}, 'name': 'topics/topic'}})
'scale': 0,
'scroll': 0,
'width': 1024,
'height': 768})
def test_invalid_slide_on_default_projector(self): def test_invalid_slide_on_default_projector(self):
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
@ -47,9 +41,11 @@ class ProjectorAPI(TestCase):
default_projector.save() default_projector.save()
response = self.client.get(reverse('projector-detail', args=['1'])) response = self.client.get(reverse('projector-detail', args=['1']))
content = json.loads(response.content.decode())
del content['projectiondefaults']
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode()), { self.assertEqual(content, {
'id': 1, 'id': 1,
'elements': { 'elements': {
'fc6ef43b624043068c8e6e7a86c5a1b0': 'fc6ef43b624043068c8e6e7a86c5a1b0':
@ -58,6 +54,8 @@ class ProjectorAPI(TestCase):
'error': 'Projector element does not exist.'}}, 'error': 'Projector element does not exist.'}},
'scale': 0, 'scale': 0,
'scroll': 0, 'scroll': 0,
'name': 'Default projector',
'blank': False,
'width': 1024, 'width': 1024,
'height': 768}) 'height': 768})