Mediafile pdf presentation with angular-pdf and pdf.js (Fixes #1664).
Thanks to André Böhlke for contribution core functionality! Some additional template improvements by Emanuel.
This commit is contained in:
parent
3ba93c2352
commit
d3ed15db29
@ -31,9 +31,10 @@
|
|||||||
"ckeditor": "~4.5.4",
|
"ckeditor": "~4.5.4",
|
||||||
"angular-ckeditor": "~1.0.0",
|
"angular-ckeditor": "~1.0.0",
|
||||||
"roboto-condensed": "~0.3.0",
|
"roboto-condensed": "~0.3.0",
|
||||||
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1"
|
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1",
|
||||||
|
"angular-pdf": "~1.3.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"angular": "^1.2.x"
|
"angular": ">=1.4.9 <1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
gulpfile.js
10
gulpfile.js
@ -21,6 +21,7 @@ var argv = require('yargs').argv,
|
|||||||
mainBowerFiles = require('main-bower-files'),
|
mainBowerFiles = require('main-bower-files'),
|
||||||
minifyCSS = require('gulp-minify-css'),
|
minifyCSS = require('gulp-minify-css'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
|
rename = require("gulp-rename"),
|
||||||
through = require('through2'),
|
through = require('through2'),
|
||||||
uglify = require('gulp-uglify'),
|
uglify = require('gulp-uglify'),
|
||||||
vsprintf = require('sprintf-js').vsprintf;
|
vsprintf = require('sprintf-js').vsprintf;
|
||||||
@ -69,6 +70,13 @@ gulp.task('ckeditor', function () {
|
|||||||
.pipe(gulp.dest(path.join(output_directory, 'ckeditor')));
|
.pipe(gulp.dest(path.join(output_directory, 'ckeditor')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extra task only for pdfjs
|
||||||
|
gulp.task('pdfjs', function () {
|
||||||
|
return gulp.src(path.join('bower_components', 'pdfjs-dist', 'build', 'pdf.worker.js'))
|
||||||
|
.pipe(rename(path.join('openslides-libs.worker.js')))
|
||||||
|
.pipe(gulp.dest(path.join(output_directory, 'js')));
|
||||||
|
});
|
||||||
|
|
||||||
// Compiles translation files (*.po) to *.json and saves them in the directory
|
// Compiles translation files (*.po) to *.json and saves them in the directory
|
||||||
// openslides/static/i18n/.
|
// openslides/static/i18n/.
|
||||||
gulp.task('translations', function () {
|
gulp.task('translations', function () {
|
||||||
@ -80,7 +88,7 @@ gulp.task('translations', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Gulp default task. Runs all other tasks before.
|
// Gulp default task. Runs all other tasks before.
|
||||||
gulp.task('default', ['js-libs', 'css-libs', 'fonts-libs', 'ckeditor', 'translations'], function () {});
|
gulp.task('default', ['js-libs', 'css-libs', 'fonts-libs', 'ckeditor', 'pdfjs', 'translations'], function () {});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,6 +10,7 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
'ngSanitize', // TODO: only use this in functions that need it.
|
'ngSanitize', // TODO: only use this in functions that need it.
|
||||||
'ui.bootstrap',
|
'ui.bootstrap',
|
||||||
'ui.tree',
|
'ui.tree',
|
||||||
|
'pdf'
|
||||||
])
|
])
|
||||||
|
|
||||||
.config([
|
.config([
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<link rel="stylesheet" href="static/css/projector.css">
|
<link rel="stylesheet" href="static/css/projector.css">
|
||||||
<link rel="icon" href="/static/img/favicon.png">
|
<link rel="icon" href="/static/img/favicon.png">
|
||||||
<script src="static/js/openslides-libs.js"></script>
|
<script src="static/js/openslides-libs.js"></script>
|
||||||
|
<script src="static/js/openslides-libs.worker.js"></script>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
#header, #footer {
|
#header, #footer {
|
||||||
|
@ -2,6 +2,7 @@ import mimetypes
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models as dbmodels
|
from django.db import models as dbmodels
|
||||||
|
from PyPDF2 import PdfFileReader
|
||||||
|
|
||||||
from ..utils.rest_api import FileField, ModelSerializer, SerializerMethodField
|
from ..utils.rest_api import FileField, ModelSerializer, SerializerMethodField
|
||||||
from .models import Mediafile
|
from .models import Mediafile
|
||||||
@ -17,10 +18,14 @@ class AngularCompatibleFileField(FileField):
|
|||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
return {
|
filetype = mimetypes.guess_type(value.path)[0]
|
||||||
|
result = {
|
||||||
'name': value.name,
|
'name': value.name,
|
||||||
'type': mimetypes.guess_type(value.path)[0]
|
'type': filetype
|
||||||
}
|
}
|
||||||
|
if filetype == 'application/pdf':
|
||||||
|
result['pages'] = PdfFileReader(open(value.path, 'rb')).getNumPages()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class MediafileSerializer(ModelSerializer):
|
class MediafileSerializer(ModelSerializer):
|
||||||
|
@ -2,6 +2,57 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('OpenSlidesApp.mediafiles.projector', ['OpenSlidesApp.mediafiles']);
|
angular.module('OpenSlidesApp.mediafiles.projector', ['OpenSlidesApp.mediafiles'])
|
||||||
|
|
||||||
}());
|
.config([
|
||||||
|
'slidesProvider',
|
||||||
|
function(slidesProvider) {
|
||||||
|
slidesProvider.registerSlide('mediafiles/mediafile', {
|
||||||
|
template: 'static/templates/mediafiles/slide_mediafile.html'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
.controller('SlideMediafileCtrl', [
|
||||||
|
'$scope',
|
||||||
|
'Mediafile',
|
||||||
|
function($scope, Mediafile) {
|
||||||
|
// load mediafile object
|
||||||
|
var mediafile = Mediafile.find($scope.element.id);
|
||||||
|
mediafile.then(function(mediafile) {
|
||||||
|
$scope.pdfName = mediafile.title;
|
||||||
|
$scope.pdfUrl = mediafile.mediafileUrl;
|
||||||
|
})
|
||||||
|
// get page from projector
|
||||||
|
$scope.page = $scope.element.page;
|
||||||
|
$scope.scroll = 0;
|
||||||
|
|
||||||
|
function updateScale() {
|
||||||
|
if($scope.element.pageFit) {
|
||||||
|
$scope.scale = 'page-fit';
|
||||||
|
} else {
|
||||||
|
$scope.scale = $scope.element.scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$watch(function() {
|
||||||
|
return $scope.element.scale;
|
||||||
|
}, updateScale);
|
||||||
|
|
||||||
|
updateScale();
|
||||||
|
|
||||||
|
$scope.getNavStyle = function(scroll) {
|
||||||
|
if (scroll > 100) {
|
||||||
|
return 'pdf-controls fixed';
|
||||||
|
} else {
|
||||||
|
return 'pdf-controls';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.onLoad = function() {
|
||||||
|
$scope.loading = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
})();
|
||||||
|
@ -51,7 +51,8 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
|
|||||||
'Mediafile',
|
'Mediafile',
|
||||||
'MediafileForm',
|
'MediafileForm',
|
||||||
'User',
|
'User',
|
||||||
function($scope, $http, ngDialog, Mediafile, MediafileForm, User) {
|
'Projector',
|
||||||
|
function($scope, $http, ngDialog, Mediafile, MediafileForm, User, Projector) {
|
||||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||||
User.bindAll({}, $scope, 'users');
|
User.bindAll({}, $scope, 'users');
|
||||||
|
|
||||||
@ -59,6 +60,20 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
|
|||||||
$scope.sortColumn = 'title';
|
$scope.sortColumn = 'title';
|
||||||
$scope.filterPresent = '';
|
$scope.filterPresent = '';
|
||||||
$scope.reverse = false;
|
$scope.reverse = false;
|
||||||
|
|
||||||
|
function updatePresentedMediafiles() {
|
||||||
|
var projectorElements = _.map(Projector.get(1).elements, function(element) { return element });
|
||||||
|
$scope.presentedMediafiles = _.filter(projectorElements, function (element) {
|
||||||
|
return element.name === 'mediafiles/mediafile';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$watch(function() {
|
||||||
|
return Projector.get(1).elements;
|
||||||
|
}, updatePresentedMediafiles);
|
||||||
|
|
||||||
|
updatePresentedMediafiles();
|
||||||
|
|
||||||
// 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 ) {
|
||||||
@ -111,6 +126,105 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
|
|||||||
$scope.delete = function (mediafile) {
|
$scope.delete = function (mediafile) {
|
||||||
Mediafile.destroy(mediafile.id);
|
Mediafile.destroy(mediafile.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ** PDF presentation functions **/
|
||||||
|
// show document on projector
|
||||||
|
$scope.showPdf = function (mediafile) {
|
||||||
|
var postUrl,
|
||||||
|
data;
|
||||||
|
if ($scope.presentedMediafiles.length > 0) {
|
||||||
|
// update first mediafile, at the moment there should not be more
|
||||||
|
var uuid = $scope.presentedMediafiles[0].uuid;
|
||||||
|
postUrl = '/rest/core/projector/1/update_elements/';
|
||||||
|
data = {};
|
||||||
|
data[uuid] = {
|
||||||
|
id: mediafile.id,
|
||||||
|
numPages: mediafile.mediafile.pages,
|
||||||
|
page: 1,
|
||||||
|
pageFit: true,
|
||||||
|
scale: 1,
|
||||||
|
visible: true
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
postUrl = '/rest/core/projector/1/prune_elements/';
|
||||||
|
data = [{
|
||||||
|
name: 'mediafiles/mediafile',
|
||||||
|
id: mediafile.id,
|
||||||
|
numPages: mediafile.mediafile.pages,
|
||||||
|
visible: true,
|
||||||
|
pageFit: true,
|
||||||
|
scale: 1,
|
||||||
|
page: 1
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
$http.post(postUrl, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendMediafileCommand(data) {
|
||||||
|
var mediafileElement = getCurrentlyPresentedMediafile();
|
||||||
|
var updateData = _.extend({}, mediafileElement);
|
||||||
|
_.extend(updateData, data);
|
||||||
|
var postData = {};
|
||||||
|
postData[mediafileElement.uuid] = updateData;
|
||||||
|
$http.post('/rest/core/projector/1/update_elements/', postData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentlyPresentedMediafile() {
|
||||||
|
return $scope.presentedMediafiles[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.mediafileGoPrevious = function () {
|
||||||
|
var mediafileElement = getCurrentlyPresentedMediafile();
|
||||||
|
if (mediafileElement.page > 1) {
|
||||||
|
sendMediafileCommand({
|
||||||
|
page: parseInt(mediafileElement.page) - 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$scope.mediafileGoNext = function () {
|
||||||
|
var mediafileElement = getCurrentlyPresentedMediafile();
|
||||||
|
if (mediafileElement.page < mediafileElement.numPages) {
|
||||||
|
sendMediafileCommand({
|
||||||
|
page: parseInt(mediafileElement.page) + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$scope.mediafileZoomIn = function () {
|
||||||
|
var mediafileElement = getCurrentlyPresentedMediafile();
|
||||||
|
sendMediafileCommand({
|
||||||
|
pageFit: false,
|
||||||
|
scale: parseFloat(mediafileElement.scale) + 0.2
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.mediafileFit = function () {
|
||||||
|
sendMediafileCommand({
|
||||||
|
pageFit: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.mediafileZoomOut = function () {
|
||||||
|
var mediafileElement = getCurrentlyPresentedMediafile();
|
||||||
|
sendMediafileCommand({
|
||||||
|
pageFit: false,
|
||||||
|
scale: parseFloat(mediafileElement.scale) - 0.2
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.mediafileChangePage = function(pageNum) {
|
||||||
|
sendMediafileCommand({
|
||||||
|
pageToDisplay: pageNum
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.mediafileRotate = function () {
|
||||||
|
var rotation;
|
||||||
|
var currentRotation = $scope.mediafile.rotation;
|
||||||
|
if (currentRotation === 270) {
|
||||||
|
rotation = 0;
|
||||||
|
} else {
|
||||||
|
rotation = currentRotation + 90;
|
||||||
|
}
|
||||||
|
sendMediafileCommand({
|
||||||
|
rotation: rotation
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -60,6 +60,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- mediafile pdf controls -->
|
||||||
|
<div os-perms="core.can_manage_projector" ng-show="presentedMediafiles.length">
|
||||||
|
<div ng-repeat="presentedMediafile in presentedMediafiles" class="well well-sm">
|
||||||
|
<h3 translate>PDF control elements</h3>
|
||||||
|
<!-- TODO: show filename / title of mediafile -->
|
||||||
|
<nav ng-class="getNavStyle(scroll)" class="form-inline">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-default" ng-click="mediafileGoPrevious()" title="{{ 'Previouse page' | translate }}">
|
||||||
|
<i class="fa fa-backward"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" ng-click="mediafileGoNext()" title="{{ 'Next page' | translate }}">
|
||||||
|
<i class="fa fa-forward"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-default" ng-click="mediafileZoomOut()" title="{{ 'Zoom out' | translate }}">
|
||||||
|
<i class="fa fa-search-minus"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" ng-click="mediafileFit()" title="{{ 'Reset zoom' | translate }}">
|
||||||
|
100%
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" ng-click="mediafileZoomIn()" title="{{ 'Zoom in' | translate }}">
|
||||||
|
<i class="fa fa-search-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon" translate>Page</span>
|
||||||
|
<input type="number" min=1 ng-model="presentedMediafile.page" class="form-control" style="width: 80px">
|
||||||
|
<span class="input-group-addon"><translate>of</translate> {{presentedMediafile.numPages}}</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="spacer-top-lg italic">
|
<div class="spacer-top-lg italic">
|
||||||
{{ mediafilesFiltered.length }} /
|
{{ mediafilesFiltered.length }} /
|
||||||
{{ mediafiles.length }} {{ "files" | translate }}<span ng-if="(mediafiles|filter:{selected:true}).length > 0">,
|
{{ mediafiles.length }} {{ "files" | translate }}<span ng-if="(mediafiles|filter:{selected:true}).length > 0">,
|
||||||
@ -110,14 +144,12 @@
|
|||||||
class="animate-item"
|
class="animate-item"
|
||||||
ng-class="{ 'activeline': mediafile.isProjected(), 'selected': mediafile.selected }">
|
ng-class="{ 'activeline': mediafile.isProjected(), 'selected': mediafile.selected }">
|
||||||
<!-- projector column -->
|
<!-- projector column -->
|
||||||
<!-- TOOD: implement project pdf feature -->
|
|
||||||
<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"
|
<a class="btn btn-default btn-sm"
|
||||||
ng-if="mediafile.mediafile.type == 'application/pdf'"
|
ng-if="mediafile.mediafile.type == 'application/pdf'"
|
||||||
ng-class="{ 'btn-primary': mediafile.isProjected() }"
|
ng-class="{ 'btn-primary': mediafile.isProjected() }"
|
||||||
ng-click="mediafile.project()"
|
ng-click="showPdf(mediafile)"
|
||||||
ng-bootbox-alert="{{ 'Sorry, the function to project pdf files is not yet implemented.' | translate }}"
|
|
||||||
title="{{ 'Project mediafile' | translate }}">
|
title="{{ 'Project mediafile' | translate }}">
|
||||||
<i class="fa fa-video-camera"></i>
|
<i class="fa fa-video-camera"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<div ng-controller="SlideMediafileCtrl" class="content">
|
||||||
|
<ng-pdf template-url="/static/templates/mediafiles/slide_mediafile_partial.html"
|
||||||
|
scale="page-fit"
|
||||||
|
ng-attr-scale="{{ scale }}"
|
||||||
|
ng-attr-page="{{ page }}"
|
||||||
|
debug="true">
|
||||||
|
</ng-pdf>
|
||||||
|
</div>
|
@ -0,0 +1 @@
|
|||||||
|
<canvas id="pdf-canvas" class="rotate0"></canvas>
|
@ -7,13 +7,14 @@
|
|||||||
"gulp-angular-gettext": "~2.1.0",
|
"gulp-angular-gettext": "~2.1.0",
|
||||||
"gulp-concat": "~2.6.0",
|
"gulp-concat": "~2.6.0",
|
||||||
"gulp-if": "~2.0.0",
|
"gulp-if": "~2.0.0",
|
||||||
|
"gulp-jshint": "~2.0.0",
|
||||||
"gulp-minify-css": "~1.2.3",
|
"gulp-minify-css": "~1.2.3",
|
||||||
|
"gulp-rename": "~1.2.2",
|
||||||
"gulp-uglify": "~1.5.1",
|
"gulp-uglify": "~1.5.1",
|
||||||
"main-bower-files": "~2.11.1",
|
"main-bower-files": "~2.11.1",
|
||||||
"yargs": "~3.32.0",
|
|
||||||
"po2json": "~0.4.1",
|
"po2json": "~0.4.1",
|
||||||
"gulp-jshint": "~2.0.0",
|
"sprintf-js": "~1.0.3",
|
||||||
"through2": "~2.0.0",
|
"through2": "~2.0.0",
|
||||||
"sprintf-js": "~1.0.3"
|
"yargs": "~3.32.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,4 +9,5 @@ reportlab>=3.0,<3.3
|
|||||||
roman>=2.0,<2.1
|
roman>=2.0,<2.1
|
||||||
setuptools>=2.2,<20.0
|
setuptools>=2.2,<20.0
|
||||||
sockjs-tornado>=1.0,<1.1
|
sockjs-tornado>=1.0,<1.1
|
||||||
Whoosh>=2.7,<2.8
|
Whoosh>=2.7.0,<2.8
|
||||||
|
PyPDF2==1.25.1
|
||||||
|
Loading…
Reference in New Issue
Block a user