Added support for multiple projectors.

This commit is contained in:
Finn Stutzenstein 2016-09-12 11:05:34 +02:00 committed by Norman Jäckel
parent 447d475321
commit e6b9b21d41
51 changed files with 2272 additions and 813 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) {
return $http.post( var isProjectedId = this.isProjected(tree);
'/rest/core/projector/1/prune_elements/', if (isProjectedId > 0) {
[{name: this.content_object.collection, id: this.content_object.id}] // 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(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{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 predicate = function (element) {
var value;
if (typeof list === 'undefined') {
// Releated item detail slide
value = element.name == self.content_object.collection &&
typeof element.id !== 'undefined' &&
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;
};
isProjected = typeof _.findKey(projector.elements, predicate) === 'string';
} else {
isProjected = false;
} }
var self = this;
var predicate = function (element) {
var value;
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
value = element.name == self.content_object.collection &&
typeof element.id !== 'undefined' &&
element.id == self.content_object.id;
}
return value;
};
var isProjected = 0;
Projector.getAll().forEach(function (projector) {
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) {
return $http.post( var isProjectedId = this.isListOfSpeakersProjected();
'/rest/core/projector/1/prune_elements/', if (isProjectedId > 0) {
[{name: 'agenda/list-of-speakers', id: this.id}] // Deactivate
); $http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
// Activate
if (isProjectedId != projectorId) {
return $http.post(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{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 + '/prune_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: '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,69 @@ 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; $scope.$watch(function() {
_.forEach(projector.elements, function(element) { return Projector.lastModified();
switch(element.name) { }, function() {
case 'motions/motion': $scope.projectors = Projector.getAll();
Motion.find(element.id).then(function(motion) { $scope.updateCurrentListOfSpeakers();
Motion.loadRelations(motion, 'agenda_item').then(function() { });
$scope.AgendaItem = motion.agenda_item; $scope.$watch(function () {
}); return Projector.lastModified();
}); }, function () {
break; var projectiondefault = ProjectionDefault.filter({name: 'current_list_of_speakers'})[0];
case 'topics/topic': if (projectiondefault) {
Topic.find(element.id).then(function(topic) { $scope.defaultProjectorId = projectiondefault.projector_id;
Topic.loadRelations(topic, 'agenda_item').then(function() { }
$scope.AgendaItem = topic.agenda_item; });
});
}); $scope.updateCurrentListOfSpeakers = function () {
break; var itemPromise = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference);
case 'assignments/assignment': if (itemPromise) {
Assignment.find(element.id).then(function(assignment) { itemPromise.then(function(item) {
Assignment.loadRelations(assignment, 'agenda_item').then(function() { $scope.AgendaItem = item;
$scope.AgendaItem = assignment.agenda_item;
});
});
break;
case 'agenda/list-of-speakers':
Agenda.find(element.id).then(function(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 + '/prune_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,48 +1,61 @@
<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> <button type="button" class="btn btn-default btn-sm"
<button os-perms="agenda.can_manage" title="{{ 'Project current list of speakers' | translate }}"
ng-click="goToListOfSpeakers()" ng-click="projectCurrentLoS(defaultProjectorId)"
class="btn btn-sm btn-default"> ng-class="{ 'btn-primary': isCurrentLoSProjected() > 0 && isCurrentLoSProjected() == defaultProjectorId}">
<i class="fa fa-microphone"></i> <i class="fa fa-video-camera"></i>
<translate>Manage list</translate> <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"
ng-click="goToListOfSpeakers()"
class="btn btn-sm btn-default">
<i class="fa fa-microphone"></i>
<translate>Manage list</translate>
</button>
</div>
</div> </div>
<h1 translate>List of speakers</h1> <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"> <!-- Last speakers -->
<div ng-if="isFullScreen" class="fullscreendiv-title"> <p ng-repeat="speaker in lastSpeakers = (AgendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
<h1 translate>List of speakers</h1> limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers">
<h2> {{ AgendaItem.getTitle() }} {{ speaker.user.get_full_name() }}
<span class="slimlabel label label-danger ng-scope" style="" <!-- Current speaker -->
ng-if="AgendaItem.speaker_list_closed" translate>Closed</span> <p ng-repeat="speaker in currentspeakers = (AgendaItem.speakers| filter: {end_time: null, begin_time: '!!'})"
</h2> class="currentSpeaker">
</div> <i class="fa fa-microphone fa-lg"></i> {{ speaker.user.get_full_name() }}
<div class="content"> <!-- Next speakers -->
<!-- Last speakers --> <ol class="nextSpeakers">
<p ng-repeat="speaker in lastSpeakers = (AgendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) | <li ng-repeat="speaker in AgendaItem.speakers | filter: {begin_time: null} | orderBy:'weight'">
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers"> {{ speaker.user.get_full_name() }}
{{ speaker.user.get_full_name() }} </ol>
<!-- 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>
</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"
<i class="fa fa-video-camera"></i> os-perms="core.can_manage_projector">
<translate>List of speakers</translate> <button type="button" class="btn btn-default btn-sm"
</a> ng-click="item.projectListOfSpeakers(defaultProjectorListOfSpeakersId)"
ng-class="{ 'btn-primary': item.isListOfSpeakersProjected() == defaultProjectorListOfSpeakersId }">
<i class="fa fa-video-camera"></i>
<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>
</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,42 +267,47 @@ 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) {
return $http.post( var isProjectedId = this.isProjected(pollId);
'/rest/core/projector/1/prune_elements/', if (isProjectedId > 0) {
[{name: 'assignments/assignment', id: this.id, poll: poll_id}] $http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
); }
if (isProjectedId != projectorId) {
return $http.post(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{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 self = this;
var isProjected; var predicate = function (element) {
if (typeof projector !== 'undefined') { var value;
var self = this; if (typeof poll_id === 'undefined') {
var predicate = function (element) { // Assignment detail slide without poll
var value; value = element.name == 'assignments/assignment' &&
if (typeof poll_id === 'undefined') { typeof element.id !== 'undefined' &&
// Assignment detail slide without poll element.id == self.id &&
value = element.name == 'assignments/assignment' && typeof element.poll === 'undefined';
typeof element.id !== 'undefined' && } else {
element.id == self.id && // Assignment detail slide with specific poll
typeof element.poll === 'undefined'; value = element.name == 'assignments/assignment' &&
} else { typeof element.id !== 'undefined' &&
// Assignment detail slide with specific poll element.id == self.id &&
value = element.name == 'assignments/assignment' && typeof element.poll !== 'undefined' &&
typeof element.id !== 'undefined' && element.poll == poll_id;
element.id == self.id && }
typeof element.poll !== 'undefined' && return value;
element.poll == poll_id; };
} var isProjected = 0;
return value; Projector.getAll().forEach(function (projector) {
}; if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjected = typeof _.findKey(projector.elements, predicate) === 'string'; isProjected = projector.id;
} else { }
isProjected = false; });
}
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='Defaultprojector')
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,33 @@ class Projector(RESTModelMixin, models.Model):
return result return result
class ProjectionDefault(RESTModelMixin, models.Model):
"""
Model for the ProjectionDefaults like Motion, Agenda, List of speakers,...
The name is the technical name like 'topics', '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,50 +65,76 @@ 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
projector_config = {} projectors = Projector.objects.all()
found = False lowest_index = None
for key, value in projector_instance.config.items(): if projectors[0]:
if value['name'] == cls.name: for key, value in projectors[0].config.items():
if index == 0: if value['name'] == cls.name:
try: if lowest_index is None or value['index'] < lowest_index:
cls.validate_config(value) lowest_index = value['index']
except ProjectorException:
# Do not proceed if the specific procjector config data is invalid. if lowest_index is None:
# The variable found remains False. # create a countdown
break for projector in projectors:
found = True projector_config = {}
if action == 'start' and value['status'] == 'stop': for key, value in projector.config.items():
value['status'] = 'running' projector_config[key] = value
value['countdown_time'] = now().timestamp() + value['countdown_time'] # new countdown
elif action == 'stop' and value['status'] == 'running': countdown = {
value['status'] = 'stop' 'name': 'core/countdown',
value['countdown_time'] = value['countdown_time'] - now().timestamp() 'stable': True,
elif action == 'reset': 'index': 1,
value['status'] = 'stop' 'default_time': config['projector_default_countdown'],
value['countdown_time'] = value.get('default', config['projector_default_countdown']) 'visible': False,
else: 'selected': True,
index += -1 }
projector_config[key] = value if action == 'start':
if found: countdown['running'] = True
projector_instance.config = projector_config countdown['countdown_time'] = now().timestamp() + countdown['default_time']
projector_instance.save() 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 = {}
found = False
for key, value in projector.config.items():
if value['name'] == cls.name and value['index'] == lowest_index:
try:
cls.validate_config(value)
except ProjectorException:
# Do not proceed if the specific procjector config data is invalid.
# The variable found remains False.
break
found = True
if action == 'start':
value['running'] = True
value['countdown_time'] = now().timestamp() + value['default_time']
elif action == 'stop' and value['running']:
value['running'] = False
value['countdown_time'] = value['countdown_time'] - now().timestamp()
elif action == 'reset':
value['running'] = False
value['countdown_time'] = value['default_time']
projector_config[key] = value
if found:
projector.config = projector_config
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', 'projectiondefaults')
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,56 @@ 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_item
- assignments
- mediafiles
- motion
- users
- list_of_speakers
- current_list_of_speakers
"""
# Check whether ProjectionDefaults exists
if ProjectionDefault.objects.all().exists():
# Do completely nothing if the 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='list_of_speakers',
display_name='List of speakers',
projector=default_projector)
ProjectionDefault.objects.create(
name='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.');
} }
@ -246,7 +254,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 +313,91 @@ angular.module('OpenSlidesApp.core', [
} }
]) ])
// This places a Projectorbutton 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 case 'motion'). Also a defaultProjectionId has to
// be given. In the Exable its a scope variable. The next two parameters are additional:
// - additional-id: Then the model.project and model.isProjected will be called whith this
// argument (ex.: model.project(2))
// - content: A not trusted 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) {
return $http.post( // if this object is already projected on projectorId, delete this element from this projector
'/rest/core/projector/1/prune_elements/', var isProjectedId = this.isProjected();
[{name: this.getResourceName(), id: this.id}] if (isProjectedId > 0) {
); $http.post('/rest/core/projector/' + isProjectedId + '/prune_elements/');
}
// if it was the same projector before, just delete it but not show again
if (isProjectedId != projectorId) {
return $http.post(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{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; };
}; var isProjectedId = 0;
isProjected = typeof _.findKey(projector.elements, predicate) === 'string'; Projector.getAll().forEach(function (projector) {
} else { if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjected = false; isProjectedId = projector.id;
} }
return isProjected; });
return isProjectedId;
}; };
return BaseModel; return BaseModel;
} }
@ -396,10 +461,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;
$.each(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',
}
}
}
}); });
} }
]) ])
@ -522,8 +651,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,25 +59,31 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Projector Container Controller // Projector Container Controller
.controller('ProjectorContainerCtrl', [ .controller('ProjectorContainerCtrl', [
'$scope', '$scope',
'Config', '$location',
'loadGlobalData', 'loadGlobalData',
function($scope, Config, loadGlobalData) { 'Projector',
'ProjectorID',
function($scope, $location, 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} Projector.find($scope.projector_id).then(function (projector) {
if (typeof $scope.config === 'function') { $scope.projectorWidth = projector.width;
var conf = $scope.config('projector_resolution'); $scope.projectorHeight = projector.height;
if(!last_conf || last_conf.width != conf.width || last_conf.height != conf.height) { $scope.recalculateIframe();
last_conf = conf; }, function (error) {
$scope.projectorWidth = conf.width; if (error.status == 404) {
$scope.projectorHeight = conf.height; $scope.error = 'Projector not found.';
$scope.recalculateIframe(); } else if (error.status == 403) {
$scope.error = 'You have to login to see the projector.';
} }
} });
}); });
// recalculate the actual Iframesize and scale // recalculate the actual Iframesize and scale
@ -114,27 +120,84 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.controller('ProjectorCtrl', [ .controller('ProjectorCtrl', [
'$scope', '$scope',
'$location',
'Projector', 'Projector',
'slides', 'slides',
function($scope, Projector, slides) { 'Config',
'ProjectorID',
function($scope, $location, Projector, slides, Config, ProjectorID) {
var projector_id = ProjectorID();
$scope.broadcast = 0;
var setElements = function (projector) {
$scope.elements = [];
_.forEach(slides.getElements(projector), function(element) {
if (!element.error) {
$scope.elements.push(element);
} else {
console.error("Error for slide " + element.name + ": " + element.error);
}
});
};
$scope.$watch(function () { $scope.$watch(function () {
// TODO: Use the current projector. At the moment there is only one. return Projector.lastModified(projector_id);
return Projector.lastModified(1);
}, function () { }, function () {
// TODO: Use the current projector. At the moment there is only one $scope.projector = Projector.get(projector_id);
var projector = Projector.get(1); if ($scope.projector) {
if (projector) { if ($scope.broadcast === 0) {
setElements($scope.projector);
$scope.blank = $scope.projector.blank;
}
} else {
// Blank projector on error
$scope.elements = []; $scope.elements = [];
_.forEach(slides.getElements(projector), function(element) { $scope.projector = {
if (!element.error) { scroll: 0,
$scope.elements.push(element); scale: 0,
} else { blank: true
console.error("Error for slide " + element.name + ": " + element.error); };
}
});
$scope.$watch(function () {
return Config.lastModified('projector_broadcast');
}, function () {
Config.findAll().then(function () {
var bc = Config.get('projector_broadcast').value;
if ($scope.broadcast != bc) {
$scope.broadcast = bc;
if ($scope.broadcastDeregister) {
// revert to original $scope.projector
$scope.broadcastDeregister();
$scope.broadcastDeregister = null;
setElements($scope.projector);
$scope.blank = $scope.projector.blank;
} }
}); }
// TODO: Use the current projector. At the moment there is only one
$scope.scroll = -80 * Projector.get(1).scroll; if ($scope.broadcast > 0) {
$scope.scale = 100 + 20 * Projector.get(1).scale; // 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);
Projector.find($scope.broadcast).then(function (broadcast_projector) {
setElements(broadcast_projector);
$scope.blank = broadcast_projector.blank;
});
}
});
}
});
});
$scope.$on('$destroy', function() {
if ($scope.broadcastDeregister) {
$scope.broadcastDeregister();
$scope.broadcastDeregister = null;
} }
}); });
} }
@ -158,13 +221,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 +250,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.$watch(function () { $scope.highestCountdownIndex = 0;
return Projector.lastModified(1); $scope.messages = [];
}, function () { $scope.highestMessageIndex = 0;
// stop ALL interval timer
for (var i=0; i<$scope.countdowns.length; i++) { var cancelIntervalTimers = function () {
if ( $scope.countdowns[i].interval ) { $scope.countdowns.forEach(function (countdown) {
$interval.cancel($scope.countdowns[i].interval); $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 () {
return Projector.lastModified();
}, function () {
$scope.projectors = Projector.getAll();
if (!$scope.active_projector) {
$scope.changeProjector($scope.projectors[0]);
} }
// rebuild all variables after projector update
$scope.rebuildAllElements(); // stop ALL interval timer
cancelIntervalTimers();
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) {
var data = {}; $scope.projectors.forEach(function (projector) {
// calculate end point of countdown (in seconds!) _.forEach(projector.elements, function (element, uuid) {
var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time; if (element.name == 'core/countdown' && element.index == countdown.index) {
data[countdown.uuid] = { var data = {};
"status": "running", // calculate end point of countdown (in seconds!)
"countdown_time": endTimestamp var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time;
}; data[uuid] = {
$http.post('/rest/core/projector/1/update_elements/', data); 'running': true,
'countdown_time': endTimestamp
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
}; };
$scope.stopCountdown = function (countdown) { $scope.stopCountdown = function (countdown) {
var data = {}; $scope.projectors.forEach(function (projector) {
// calculate rest duration of countdown (in seconds!) _.forEach(projector.elements, function (element, uuid) {
var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset ); if (element.name == 'core/countdown' && element.index == countdown.index) {
data[countdown.uuid] = { var data = {};
"status": "stop", // calculate rest duration of countdown (in seconds!)
"countdown_time": newDuration var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
}; data[uuid] = {
$http.post('/rest/core/projector/1/update_elements/', data); 'running': false,
'countdown_time': newDuration
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
}; };
$scope.resetCountdown = function (countdown) { $scope.resetCountdown = function (countdown) {
var data = {}; $scope.projectors.forEach(function (projector) {
data[countdown.uuid] = { _.forEach(projector.elements, function (element, uuid) {
"status": "stop", if (element.name == 'core/countdown' && element.index == countdown.index) {
"countdown_time": countdown.default, var data = {};
}; data[uuid] = {
$http.post('/rest/core/projector/1/update_elements/', data); 'running': false,
'countdown_time': countdown.default_time,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
}; };
// *** message functions *** // *** message functions ***
$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 = {};
data[uuid] = {
message: message.message,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
$scope.addMessage = function () { $scope.addMessage = function () {
$http.post('/rest/core/projector/1/activate_elements/', [{ $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', name: 'core/message',
visible: false, visible: false,
index: $scope.messages.length, selected: true,
index: $scope.highestMessageIndex,
message: '', message: '',
stable: true stable: true,
}]); }]);
});
}; };
$scope.removeMessage = function (message) { $scope.removeMessage = function (message) {
$http.post('/rest/core/projector/1/deactivate_elements/', [message.uuid]); $scope.projectors.forEach(function (projector) {
}; _.forEach(projector.elements, function (element, uuid) {
$scope.showMessage = function (message) { if (element.name == 'core/message' && element.index == message.index) {
var data = {}; $http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
// if current message is activated, deactivate all other messages
if ( !message.visible ) {
for (var i=0; i<$scope.messages.length; i++) {
if ( $scope.messages[i].uuid == message.uuid ) {
data[$scope.messages[i].uuid] = { "visible": true };
} else {
data[$scope.messages[i].uuid] = { "visible": false };
} }
} });
} else { });
data[message.uuid] = { "visible": false };
}
$http.post('/rest/core/projector/1/update_elements/', data);
};
$scope.editMessage = function (message) {
var data = {};
data[message.uuid] = {
"message": message.message,
};
$http.post('/rest/core/projector/1/update_elements/', data);
message.editMessageFlag = false;
}; };
// *** projector controls *** /* project functions*/
$scope.scrollLevel = Projector.get(1).scroll; $scope.project = function (element) {
$scope.scaleLevel = Projector.get(1).scale; $scope.projectors.forEach(function (projector) {
$scope.controlProjector = function (action, direction) { _.forEach(projector.elements, function (projectorElement, uuid) {
$http.post('/rest/core/projector/1/control_view/', {"action": action, "direction": direction}); 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.editCurrentSlide = function () { $scope.isProjected = function (element) {
$.each(Projector.get(1).elements, function(key, value) { var projectorIds = [];
if (value.name == 'agenda/list-of-speakers') { $scope.projectors.forEach(function (projector) {
$state.go('agenda.item.detail', {id: value.id}); _.forEach(projector.elements, function (projectorElement, uuid) {
} else if ( if (element.name == projectorElement.name && element.index == projectorElement.index) {
value.name != 'agenda/item-list' && if (projectorElement.visible && projectorElement.selected) {
value.name != 'core/clock' && projectorIds.push(projector.id);
value.name != 'core/countdown' && }
value.name != 'core/message' ) { }
$state.go(value.name.replace('/', '.')+'.detail.update', {id: value.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) {
$.each(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>
<translate>{{ 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>
</a> <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>
</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>
</a> <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>
</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>Standard</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,34 +11,41 @@
<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">
#header, #footer { <style type="text/css">
background-color: {{ config('projector_header_backgroundcolor') }}; #header, #footer {
} background-color: {{ config('projector_header_backgroundcolor') }};
#header, #footer, #currentTime { }
color: {{ config('projector_header_fontcolor') }}; #header, #footer, #currentTime {
} color: {{ config('projector_header_fontcolor') }};
h1 { }
color: {{ config('projector_h1_fontcolor') }}; #header, #footer, .contentContainer {
} visibility: {{ blank ? 'hidden' : 'visible' }};
</style> }
#projectorContainer {
background-color: {{ blank ? config('projector_blank_color') : '#fff' }};
height: {{ blank ? '100%' : 'auto' }};
}
h1 {
color: {{ config('projector_h1_fontcolor') }};
}
</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') }"
ng-bind-html="config('general_event_name')"></div> ng-bind-html="config('general_event_name')"></div>
<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,21 +54,21 @@
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 id="footer">
<span ng-if="config('general_event_date')">
{{ config('general_event_date') }}
</span>
<span ng-if="config('general_event_date') && config('general_event_location')">
|
</span>
<span ng-if="config('general_event_location')">
{{ config('general_event_location') }}
</span>
</div>
</div> </div>
<div id="footer">
<span ng-if="config('general_event_date')">
{{ config('general_event_date') }}
</span>
<span ng-if="config('general_event_date') && config('general_event_location')">
|
</span>
<span ng-if="config('general_event_location')">
{{ config('general_event_location') }}
</span>
</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

@ -18,6 +18,7 @@ from django.utils.timezone import now
from openslides import __version__ as version from openslides import __version__ as version
from openslides.utils import views as utils_views from openslides.utils import views as utils_views
from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.plugins import ( from openslides.utils.plugins import (
get_plugin_description, get_plugin_description,
get_plugin_verbose_name, get_plugin_verbose_name,
@ -25,7 +26,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 +42,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 +171,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.
@ -192,13 +192,23 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
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 ('activate_elements', 'prune_elements', 'update_elements',
'deactivate_elements', 'clear_elements', 'control_view', 'deactivate_elements', 'clear_elements', 'control_view',
'set_resolution', 'set_scroll'): 'set_resolution', 'set_scroll', 'control_blank', 'destroy',
'create', 'update', '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 projectionDefaults from this projector to the default projector (pk=1)
def destroy(self, *args, **kwargs):
projector_instance = self.get_object()
for a in ProjectionDefault.objects.all():
if a.projector.id == projector_instance.id:
a.projector = Projector.objects.get(pk=1)
a.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 +457,70 @@ 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
projector_instance = self.get_object()
inform_changed_data(projector_instance)
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,19 +56,35 @@ 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() { return element.name === 'mediafiles/mediafile';
var projectorElements = _.map(Projector.get(1).elements, function(element) { return element; }); });
$scope.presentedMediafiles = _.filter(projectorElements, function (element) { mediaElements.forEach(function (element) {
return element.name === 'mediafiles/mediafile'; $scope.presentedMediafiles.push(element);
});
}); });
if ($scope.presentedMediafiles.length) { if ($scope.presentedMediafiles.length) {
$scope.isMeta = false; $scope.isMeta = false;
@ -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,37 +155,47 @@ 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();
var data = [{ if (isProjectedId > 0) {
name: 'mediafiles/mediafile', $http.post('/rest/core/projector/' + isProjectedId + '/prune_elements/', []);
id: mediafile.id, }
numPages: mediafile.mediafile.pages, if (isProjectedId != projectorId) {
page: 1, var postUrl = '/rest/core/projector/' + projectorId + '/prune_elements/';
scale: 'page-fit', var data = [{
rotate: 0, name: 'mediafiles/mediafile',
visible: true, id: mediafile.id,
playing: false, numPages: mediafile.mediafile.pages,
fullscreen: mediafile.is_pdf page: 1,
}]; scale: 'page-fit',
$http.post(postUrl, data); rotate: 0,
visible: true,
playing: false,
fullscreen: mediafile.is_pdf
}];
$http.post(postUrl, data);
}
}; };
function sendMediafileCommand(data) { // To avoid some kind of 60,000000000001% in template
var mediafileElement = getCurrentlyPresentedMediafile(); $scope.round = function (val) {return Math.round(val);};
var updateData = _.extend({}, mediafileElement);
var sendMediafileCommand = function (mediafile, data) {
var updateData = _.extend({}, mediafile);
_.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,55 +203,57 @@ 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.mediafileScroll = function(scroll) {
var mediafileElement = getCurrentlyPresentedMediafile(); var mediafileElement = getCurrentlyPresentedMediafile();

View File

@ -28,12 +28,12 @@
<h3>{{ getTitle(presentedMediafile) }}</h3> <h3>{{ getTitle(presentedMediafile) }}</h3>
<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,23 +42,23 @@
<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>
@ -195,13 +195,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.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"
<i class="fa fa-video-camera"></i> ng-click="showMediafile(defaultProjectorId, mediafile)"
</a> ng-class="{ 'btn-primary': mediafile.isProjected() == defaultProjectorId }">
<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>
</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.
@ -78,8 +79,11 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
} }
}; };
Motion.bindOne(id, $scope, 'motion'); Motion.find(id).then(function (motion) {
$scope.motion = 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,12 @@ 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 () {
console.log(ProjectionDefault.getAll());
$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

@ -138,11 +138,25 @@ 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 data
broadcast_projector = Projector.objects.get(pk=broadcast_id)
send_all = True
# The data from the broadcasted projector
broadcast_projector_data = get_projector_element_data(broadcast_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:
output = get_projector_element_data(projector) if broadcast_projector_data is None:
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

@ -14,6 +14,7 @@ from rest_framework.routers import DefaultRouter
from rest_framework.serializers import ModelSerializer as _ModelSerializer from rest_framework.serializers import ModelSerializer as _ModelSerializer
from rest_framework.serializers import ( # noqa from rest_framework.serializers import ( # noqa
MANY_RELATION_KWARGS, MANY_RELATION_KWARGS,
BooleanField,
CharField, CharField,
DictField, DictField,
Field, Field,
@ -29,8 +30,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 +173,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,9 +25,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': {
'aae4a07b26534cfb9af4232f361dce73': 'aae4a07b26534cfb9af4232f361dce73':
@ -36,6 +38,8 @@ class ProjectorAPI(TestCase):
'name': 'topics/topic'}}, 'name': 'topics/topic'}},
'scale': 0, 'scale': 0,
'scroll': 0, 'scroll': 0,
'name': 'Defaultprojector',
'blank': False,
'width': 1024, 'width': 1024,
'height': 768}) 'height': 768})
@ -47,9 +51,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 +64,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': 'Defaultprojector',
'blank': False,
'width': 1024, 'width': 1024,
'height': 768}) 'height': 768})