Custom fonts for pdf and projector

This commit is contained in:
FinnStutzenstein 2018-01-30 16:12:02 +01:00
parent 8042beda60
commit dfb40684ee
21 changed files with 493 additions and 119 deletions

View File

@ -112,6 +112,7 @@ Core:
- Set default of projector resolution to 1220x915 [#2549]. - Set default of projector resolution to 1220x915 [#2549].
- Preparations for the SAML plugin; Fixed caching of main views [#3535]. - Preparations for the SAML plugin; Fixed caching of main views [#3535].
- Removed unnecessary OPTIONS request in config [#3541]. - Removed unnecessary OPTIONS request in config [#3541].
- Added possibility to upload custom fonts for projector and pdf [#3568].
Mediafiles: Mediafiles:
- Fixed reloading of PDF on page change [#3274]. - Fixed reloading of PDF on page change [#3274].

View File

@ -80,9 +80,9 @@ gulp.task('pdf-worker', function () {
gulp.task('pdf-worker-libs', function () { gulp.task('pdf-worker-libs', function () {
return gulp.src([ return gulp.src([
path.join('bower_components', 'pdfmake', 'build', 'pdfmake.min.js'), path.join('bower_components', 'pdfmake', 'build', 'pdfmake.min.js'),
path.join('bower_components', 'pdfmake', 'build', 'vfs_fonts.js'),
]) ])
.pipe(concat('pdf-worker-libs.js')) .pipe(gulpif(argv.production, uglify()))
.pipe(rename('pdf-worker-libs.js'))
.pipe(gulp.dest(path.join(output_directory, 'js', 'workers'))); .pipe(gulp.dest(path.join(output_directory, 'js', 'workers')));
}); });

View File

@ -19,7 +19,7 @@ INPUT_TYPE_MAPPING = {
'colorpicker': str, 'colorpicker': str,
'datetimepicker': int, 'datetimepicker': int,
'majorityMethod': str, 'majorityMethod': str,
'logo': dict, 'static': dict,
'translations': list, 'translations': list,
} }
@ -125,9 +125,9 @@ class ConfigHandler:
valuecopy[id] = commentsfield valuecopy[id] = commentsfield
value = valuecopy value = valuecopy
if config_variable.input_type == 'logo': if config_variable.input_type == 'static':
if not isinstance(value, dict): if not isinstance(value, dict):
raise ConfigError(_('logo has to be a dict.')) raise ConfigError(_('This has to be a dict.'))
whitelist = ( whitelist = (
'path', 'path',
'display_name', 'display_name',

View File

@ -259,7 +259,7 @@ def get_config_variables():
default_value={ default_value={
'display_name': 'Projector logo', 'display_name': 'Projector logo',
'path': ''}, 'path': ''},
input_type='logo', input_type='static',
weight=301, weight=301,
group='Logo', group='Logo',
hidden=True) hidden=True)
@ -269,7 +269,7 @@ def get_config_variables():
default_value={ default_value={
'display_name': 'Projector header image', 'display_name': 'Projector header image',
'path': ''}, 'path': ''},
input_type='logo', input_type='static',
weight=302, weight=302,
group='Logo', group='Logo',
hidden=True) hidden=True)
@ -279,7 +279,7 @@ def get_config_variables():
default_value={ default_value={
'display_name': 'Web interface header logo', 'display_name': 'Web interface header logo',
'path': ''}, 'path': ''},
input_type='logo', input_type='static',
weight=303, weight=303,
group='Logo', group='Logo',
hidden=True) hidden=True)
@ -290,7 +290,7 @@ def get_config_variables():
default_value={ default_value={
'display_name': 'PDF header logo', 'display_name': 'PDF header logo',
'path': ''}, 'path': ''},
input_type='logo', input_type='static',
weight=310, weight=310,
group='Logo', group='Logo',
hidden=True) hidden=True)
@ -300,7 +300,7 @@ def get_config_variables():
default_value={ default_value={
'display_name': 'PDF footer logo', 'display_name': 'PDF footer logo',
'path': ''}, 'path': ''},
input_type='logo', input_type='static',
weight=311, weight=311,
group='Logo', group='Logo',
hidden=True) hidden=True)
@ -310,11 +310,67 @@ def get_config_variables():
default_value={ default_value={
'display_name': 'PDF ballot paper logo', 'display_name': 'PDF ballot paper logo',
'path': ''}, 'path': ''},
input_type='logo', input_type='static',
weight=312, weight=312,
group='Logo', group='Logo',
hidden=True) hidden=True)
# Fonts
yield ConfigVariable(
name='fonts_available',
default_value=[
'font_regular',
'font_italic',
'font_bold',
'font_bold_italic'],
weight=320,
group='Font',
hidden=True)
yield ConfigVariable(
name='font_regular',
default_value={
'display_name': 'Font regular',
'default': 'static/fonts/Roboto-Regular.woff',
'path': ''},
input_type='static',
weight=321,
group='Font',
hidden=True)
yield ConfigVariable(
name='font_italic',
default_value={
'display_name': 'Font italic',
'default': 'static/fonts/Roboto-Medium.woff',
'path': ''},
input_type='static',
weight=321,
group='Font',
hidden=True)
yield ConfigVariable(
name='font_bold',
default_value={
'display_name': 'Font bold',
'default': 'static/fonts/Roboto-Condensed-Regular.woff',
'path': ''},
input_type='static',
weight=321,
group='Font',
hidden=True)
yield ConfigVariable(
name='font_bold_italic',
default_value={
'display_name': 'Font bold italic',
'default': 'static/fonts/Roboto-Condensed-Light.woff',
'path': ''},
input_type='static',
weight=321,
group='Font',
hidden=True)
# Custom translations # Custom translations
yield ConfigVariable( yield ConfigVariable(
name='translations', name='translations',

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-01-30 13:00
from __future__ import unicode_literals
from django.contrib.auth.models import Permission
from django.db import migrations
def delete_old_logo_permission(apps, schema_editor):
"""
Deletes the old 'can_manage_logo' permission which is replaced with
'can_manage_logos_and_fonts'. If this is a fresh database, no permission
will be deleted, in fact the old permission does not exist. Django creates
the permission after all migration and the old one is not generated.
If this is an old database, the new permission will be created and the old
one deleted. Also it will be assigned to the groups, which had the old permission.
"""
perm = Permission.objects.filter(codename='can_manage_logos')
if len(perm):
perm = perm.get()
# Save content_type for manual creation of new permissions.
content_type = perm.content_type
# Save groups. list() is necessary to evaluate the database query right now.
groups = list(perm.group_set.all())
# Delete permission
perm.delete()
# Create new permission
perm = Permission.objects.create(
codename='can_manage_logos_and_fonts',
name='Can manage logos and fonts',
content_type=content_type)
for group in groups:
group.permissions.add(perm)
group.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0006_auto_20180123_0903'),
]
operations = [
migrations.AlterModelOptions(
name='configstore',
options={
'default_permissions': (),
'permissions': (
('can_manage_config', 'Can manage configuration'),
('can_manage_logos_and_fonts', 'Can manage logos and fonts')
)
},
),
migrations.RunPython(
delete_old_logo_permission
),
]

View File

@ -281,7 +281,7 @@ class ConfigStore(RESTModelMixin, models.Model):
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_manage_config', 'Can manage configuration'), ('can_manage_config', 'Can manage configuration'),
('can_manage_logos', 'Can manage logos')) ('can_manage_logos_and_fonts', 'Can manage logos and fonts'))
@classmethod @classmethod
def get_collection_string(cls): def get_collection_string(cls):

View File

@ -3,22 +3,22 @@
*/ */
@font-face { @font-face {
font-family: $font; font-family: $font;
src: $font-src; src: $font-src;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: $font-medium; font-family: $font-medium;
src: $font-medium-src; src: $font-medium-src;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: $font-condensed; font-family: $font-condensed;
src: $font-condensed-src; src: $font-condensed-src;
font-weight: 100; font-weight: 100;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: $font-condensed-light; font-family: $font-condensed-light;

View File

@ -1,9 +1,10 @@
/** Fonts **/ /** Fonts **/
$font: 'Roboto'; /* Note: The font naming has to be consistent to the projector.html */
$font: 'OSFont';
$font-src: url('../fonts/Roboto-Regular.woff') format('woff'); $font-src: url('../fonts/Roboto-Regular.woff') format('woff');
$font-medium: 'Roboto Medium'; $font-medium: 'OSFont Medium';
$font-medium-src: url('../fonts/Roboto-Medium.woff') format('woff'); $font-medium-src: url('../fonts/Roboto-Medium.woff') format('woff');
$font-condensed: 'Roboto Condensed'; $font-condensed: 'OSFont Condensed';
$font-condensed-src: url('../fonts/Roboto-Condensed-Regular.woff') format('woff'); $font-condensed-src: url('../fonts/Roboto-Condensed-Regular.woff') format('woff');
$font-condensed-light: 'Roboto Condensed Light'; $font-condensed-light: 'OSFont Condensed Light';
$font-condensed-light-src: url('../fonts/Roboto-Condensed-Light.woff') format('woff'); $font-condensed-light-src: url('../fonts/Roboto-Condensed-Light.woff') format('woff');

View File

@ -2,7 +2,6 @@
/* General */ /* General */
@import "variables"; @import "variables";
@import "fonts";
@import "helper"; @import "helper";
@import "ui-override"; @import "ui-override";

View File

@ -15,4 +15,3 @@
@import "../../../motions/static/css/motions/site"; @import "../../../motions/static/css/motions/site";
@import "../../../users/static/css/users/site"; @import "../../../users/static/css/users/site";
@import "../../../mediafiles/static/css/mediafiles/site"; @import "../../../mediafiles/static/css/mediafiles/site";

View File

@ -826,36 +826,78 @@ angular.module('OpenSlidesApp.core', [
getAll: function () { getAll: function () {
var self = this; var self = this;
return _.map(this.getKeys(), function (key) { return _.map(this.getKeys(), function (key) {
return self.getFromKey(key); return self.get(key);
}); });
}, },
getFromKey: function (key) { get: function (key) {
var config = Config.get(key); var config = Config.get(key);
if (config) { if (config) {
config.value.key = key; config.value.key = key;
return config.value; return config.value;
} }
}, },
isMediafileUsedAsLogo: function (mediafile) { set: function (key, path) {
return _.find(this.getAll(), function (logoPlaceholder) {
return logoPlaceholder.path === mediafile.mediafileUrl;
});
},
canMediafileBeUsedAsLogo: function (mediafile) {
return mediafile.is_image;
},
setMediafile: function (key, mediafile) {
var config = Config.get(key); var config = Config.get(key);
if (!mediafile || mediafile.canBeUsedAsLogo()) { if (config) {
config.value.path = mediafile ? mediafile.mediafileUrl : ''; config.value.path = path;// ? mediafile.mediafileUrl : '';
Config.save(key); Config.save(key);
} }
}, },
getLogosForMediafile: function (mediafile) { };
return _.filter(this.getAll(), function (logoPlaceholder) { }
return logoPlaceholder.path === mediafile.mediafileUrl; ])
.factory('Fonts', [
'Config',
'gettext',
function (Config, gettext) {
var extensionFormatMap = {
'ttf': 'truetype',
'woff': 'woff',
};
return {
getKeys: function () {
return Config.get('fonts_available').value;
},
getAll: function () {
var self = this;
return _.map(this.getKeys(), function (key) {
return self.get(key);
}); });
}, },
get: function (key) {
var config = Config.get(key);
if (config) {
config.value.key = key;
return config.value;
}
},
getUrl: function (key) {
var font = this.get(key);
if (font) {
var path = font.path;
if (!path) {
return font.default;
}
return path;
}
},
getForCss: function (key) {
var url = this.getUrl(key);
if (url) {
var ext = _.last(url.split('.'));
return "url('" + url + "') format('" +
extensionFormatMap[ext] + "')";
}
},
set: function (key, path) {
var config = Config.get(key);
if (config) {
config.value.path = path;
Config.save(key);
}
},
}; };
} }
]) ])

View File

@ -10,20 +10,18 @@ var document = {
}; };
var window = this; var window = this;
// PdfMake and Fonts // PdfMake
importScripts('/static/js/workers/pdf-worker-libs.js'); importScripts('/static/js/workers/pdf-worker-libs.js');
// Set default font family. // Set default font family.
// To use custom ttf font files you have to replace the vfs_fonts.js file. // "PdfFont" and "OSFont-*" are generic names used here and in core/pdf.js. The
// See https://github.com/pdfmake/pdfmake/wiki/Custom-Fonts---client-side // suffix after "OSFont-" has to be the same as the config value.
// "PdfFont" is used as generic name in core/pdf.js. Adjust the four
// font style names only.
pdfMake.fonts = { pdfMake.fonts = {
PdfFont: { PdfFont: {
normal: 'Roboto-Regular.ttf', normal: 'OSFont-regular.ttf',
bold: 'Roboto-Medium.ttf', bold: 'OSFont-bold.ttf',
italics: 'Roboto-Italic.ttf', italics: 'OSFont-italic.ttf',
bolditalics: 'Roboto-Italic.ttf' bolditalics: 'OSFont-bold_italic.ttf'
} }
}; };
@ -106,8 +104,9 @@ var replaceFooter = function (doc) {
// Create PDF on message and return the base64 decoded document // Create PDF on message and return the base64 decoded document
self.addEventListener('message', function(e) { self.addEventListener('message', function(e) {
var data = JSON.parse(e.data); var data = JSON.parse(e.data);
var doc = data.pdfDocument; pdfMake.vfs = data.vfs; // Set custom fonts.
var doc = data.pdfDocument;
replaceFooter(doc); replaceFooter(doc);
replacePlaceholder(doc.content); replacePlaceholder(doc.content);

View File

@ -1061,13 +1061,99 @@ angular.module('OpenSlidesApp.core.pdf', [])
} }
]) ])
// Creates the virtual filesystem for PdfMake.
.factory('PdfVfs', [
'$q',
'$http',
'Fonts',
'Config',
function ($q, $http, Fonts, Config) {
var urlCache = {}; // Caches the get request. Maps urls to base64 data ready to use.
var loadFont = function (url) {
return $q(function (resolve, reject) {
// Get font
return $http.get(url, {responseType: 'blob'}).then(function (success) {
// Convert to base64
var reader = new FileReader();
reader.readAsDataURL(success.data);
reader.onloadend = function() {
resolve(reader.result.split(',')[1]);
};
}, function (error) {
reject(error);
});
});
};
/*
* Returns a map from urls to arrays of font types used by PdfMake.
* E.g. if the font "regular" and bold" have the urls "fonts/myFont.ttf",
* the map fould be "fonts/myFont.ttf": ["OSFont-regular.ttf", "OSFont-bold.ttf"]
*/
var getUrlMapping = function () {
var urlMap = {};
var fonts = ['regular', 'italic', 'bold', 'bold_italic'];
_.forEach(fonts, function (font) {
var url = Fonts.getUrl('font_' + font);
if (!urlMap[url]) {
urlMap[url] = [];
}
urlMap[url].push('OSFont-' + font + '.ttf');
});
return urlMap;
};
/*
* Create the virtual filesystem needed by PdfMake for the fonts. Gets the url
* mapping and loads all fonts via get requests or the urlCache.
*/
var getVfs = function () {
return $q(function (resolve, reject) {
var vfs = {};
var urls = getUrlMapping();
var promises = _.chain(urls)
.map(function (filenames, url) {
if (urlCache[url]) {
// Just save the cache data into vfs.
_.forEach(filenames, function (filename) {
vfs[filename] = urlCache[url];
});
return false; // No promise here, it was all cached.
} else {
// Not in the cache, get the font and save the data into vfs.
return loadFont(url).then(function (data) {
urlCache[url] = data;
_.forEach(filenames, function (filename) {
vfs[filename] = data;
});
});
}
})
.filter(function (promise) {
return promise;
})
.value();
$q.all(promises).then(function () {
resolve(vfs);
});
});
};
return {
get: getVfs,
};
}
])
.factory('PdfCreate', [ .factory('PdfCreate', [
'$timeout', '$timeout',
'$q', '$q',
'gettextCatalog', 'gettextCatalog',
'FileSaver', 'FileSaver',
'PdfVfs',
'Messaging', 'Messaging',
function ($timeout, $q, gettextCatalog, FileSaver, Messaging) { function ($timeout, $q, gettextCatalog, FileSaver, PdfVfs, Messaging) {
var filenameMessageMap = {}; var filenameMessageMap = {};
var b64toBlob = function(b64Data) { var b64toBlob = function(b64Data) {
var byteCharacters = atob(b64Data); var byteCharacters = atob(b64Data);
@ -1105,40 +1191,46 @@ angular.module('OpenSlidesApp.core.pdf', [])
return { return {
getBase64FromDocument: function (pdfDocument) { getBase64FromDocument: function (pdfDocument) {
return $q(function (resolve, reject) { return $q(function (resolve, reject) {
var pdfWorker = new Worker('/static/js/workers/pdf-worker.js'); PdfVfs.get().then(function (vfs) {
pdfWorker.addEventListener('message', function (event) { var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
resolve(event.data); pdfWorker.addEventListener('message', function (event) {
resolve(event.data);
});
pdfWorker.addEventListener('error', function (event) {
reject(event);
});
pdfWorker.postMessage(JSON.stringify({
pdfDocument: pdfDocument,
vfs: vfs,
}));
}); });
pdfWorker.addEventListener('error', function (event) {
reject(event);
});
pdfWorker.postMessage(JSON.stringify({
pdfDocument: pdfDocument
}));
}); });
}, },
// Struckture of pdfDocuments: { filname1: doc, filename2: doc, ...} // Struckture of pdfDocuments: { filname1: doc, filename2: doc, ...}
getBase64FromMultipleDocuments: function (pdfDocuments) { getBase64FromMultipleDocuments: function (pdfDocuments) {
return $q(function (resolve, reject) { return $q(function (resolve, reject) {
var pdfWorker = new Worker('/static/js/workers/pdf-worker.js'); PdfVfs.get().then(function (vfs) {
var resultCount = 0; var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
var base64Map = {}; // Maps filename to base64 var resultCount = 0;
pdfWorker.addEventListener('message', function (event) { var base64Map = {}; // Maps filename to base64
resultCount++; pdfWorker.addEventListener('message', function (event) {
var data = JSON.parse(event.data); resultCount++;
base64Map[data.filename] = data.base64; var data = JSON.parse(event.data);
if (resultCount === _.keys(pdfDocuments).length) { base64Map[data.filename] = data.base64;
resolve(base64Map); if (resultCount === _.keys(pdfDocuments).length) {
} resolve(base64Map);
}); }
pdfWorker.addEventListener('error', function (event) { });
reject(event); pdfWorker.addEventListener('error', function (event) {
}); reject(event);
_.forEach(pdfDocuments, function (doc, filename) { });
pdfWorker.postMessage(JSON.stringify({ _.forEach(pdfDocuments, function (doc, filename) {
filename: filename, pdfWorker.postMessage(JSON.stringify({
pdfDocument: doc filename: filename,
})); pdfDocument: doc,
vfs: vfs,
}));
});
}); });
}); });
}, },

View File

@ -78,12 +78,13 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
} }
]) ])
.controller('LanguageCtrl', [ .controller('LanguageAndFontCtrl', [
'$scope', '$scope',
'Languages', 'Languages',
'Config', 'Config',
'ProjectorID', 'ProjectorID',
function ($scope, Languages, Config, ProjectorID) { 'Fonts',
function ($scope, Languages, Config, ProjectorID, Fonts) {
// for the dynamic title // for the dynamic title
$scope.projectorId = ProjectorID(); $scope.projectorId = ProjectorID();
@ -98,6 +99,18 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
} }
Languages.setCurrentLanguage($scope.selectedLanguage); Languages.setCurrentLanguage($scope.selectedLanguage);
}); });
$scope.$watch(function () {
return Config.lastModified('font_regular') +
Config.lastModified('font_italic') +
Config.lastModified('font_bold') +
Config.lastModified('font_bold_italic');
}, function () {
$scope.font = Fonts.getForCss('font_regular');
$scope.font_medium = Fonts.getForCss('font_italic');
$scope.font_condensed = Fonts.getForCss('font_bold');
$scope.font_condensed_light = Fonts.getForCss('font_bold_italic');
});
} }
]) ])

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ selectedLanguage }}" ng-controller="LanguageCtrl" class="no-js"> <html lang="{{ selectedLanguage }}" ng-controller="LanguageAndFontCtrl" class="no-js">
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/"> <base href="/">

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ selectedLanguage }}" ng-controller="LanguageCtrl" class="no-js"> <html lang="{{ selectedLanguage }}" ng-controller="LanguageAndFontCtrl" class="no-js">
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/"> <base href="/">
@ -7,6 +7,34 @@
<link rel="stylesheet" href="static/css/openslides-libs.css"> <link rel="stylesheet" href="static/css/openslides-libs.css">
<link rel="stylesheet" href="static/css/openslides-projector.css"> <link rel="stylesheet" href="static/css/openslides-projector.css">
<link rel="icon" href="/static/img/favicon.png"> <link rel="icon" href="/static/img/favicon.png">
<style type="text/css">
@font-face {
font-family: 'OSFont';
src: {{ font }};
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'OSFont Medium';
src: {{ font_medium }};
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'OSFont Condensed';
src: {{ font_condensed }};
font-weight: 100;
font-style: normal;
}
@font-face {
font-family: 'OSFont Condensed Light';
src: {{ font_condensed_light }};
font-weight: 100;
font-style: normal;
}
</style>
<script src="static/js/openslides-libs.js"></script> <script src="static/js/openslides-libs.js"></script>
<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>

View File

@ -600,11 +600,12 @@ class ConfigViewSet(ModelViewSet):
# enabled. # enabled.
result = self.request.user.is_authenticated() or anonymous_is_enabled() result = self.request.user.is_authenticated() or anonymous_is_enabled()
elif self.action in ('partial_update', 'update'): elif self.action in ('partial_update', 'update'):
# The user needs 'core.can_manage_logos' for all config values # The user needs 'core.can_manage_logos_and_fonts' for all config values
# starting with 'logo'. For all other config values th euser needs # starting with 'logo' and 'font'. For all other config values th euser needs
# the default permissions 'core.can_manage_config'. # the default permissions 'core.can_manage_config'.
if self.kwargs['pk'].startswith('logo'): pk = self.kwargs['pk']
result = has_perm(self.request.user, 'core.can_manage_logos') if pk.startswith('logo') or pk.startswith('font'):
result = has_perm(self.request.user, 'core.can_manage_logos_and_fonts')
else: else:
result = has_perm(self.request.user, 'core.can_manage_config') result = has_perm(self.request.user, 'core.can_manage_config')
else: else:

View File

@ -24,8 +24,9 @@ angular.module('OpenSlidesApp.mediafiles.list', [
'Mediafile', 'Mediafile',
'MediafileForm', 'MediafileForm',
'Logos', 'Logos',
'Fonts',
function ($http, $scope, gettext, ngDialog, osTableFilter, osTableSort, osTablePagination, function ($http, $scope, gettext, ngDialog, osTableFilter, osTableSort, osTablePagination,
ProjectionDefault, Projector, User, Mediafile, MediafileForm, Logos) { ProjectionDefault, Projector, User, Mediafile, MediafileForm, Logos, Fonts) {
$scope.$watch(function () { $scope.$watch(function () {
return Mediafile.lastModified(); return Mediafile.lastModified();
}, function () { }, function () {
@ -259,21 +260,9 @@ angular.module('OpenSlidesApp.mediafiles.list', [
); );
}; };
/** Logos **/ /** Logos and fonts **/
$scope.logos = Logos.getAll(); $scope.logos = Logos.getAll();
$scope.toggleLogo = function (mediafile, logo) { $scope.fonts = Fonts.getAll();
if (!$scope.hasLogo(mediafile, logo)) {
Logos.setMediafile(logo.key, mediafile);
} else {
Logos.setMediafile(logo.key);
}
};
$scope.hasLogo = function (mediafile, logo) {
var allUrls = _.map(mediafile.getLogos(), function (logo) {
return logo.path;
});
return _.includes(allUrls, logo.path);
};
$scope.hasProjectorHeaderLogo = function (mediafile) { $scope.hasProjectorHeaderLogo = function (mediafile) {
return _.some(mediafile.getLogos(), function (logo) { return _.some(mediafile.getLogos(), function (logo) {
return logo.key === 'logo_projector_header'; return logo.key === 'logo_projector_header';

View File

@ -13,7 +13,8 @@ angular.module('OpenSlidesApp.mediafiles.resources', [
'gettext', 'gettext',
'jsDataModel', 'jsDataModel',
'Logos', 'Logos',
function (DS, gettext, jsDataModel, Logos) { 'Fonts',
function (DS, gettext, jsDataModel, Logos, Fonts) {
var name = 'mediafiles/mediafile'; var name = 'mediafiles/mediafile';
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
@ -50,13 +51,60 @@ angular.module('OpenSlidesApp.mediafiles.resources', [
}); });
}, },
isUsedAsLogo: function () { isUsedAsLogo: function () {
return Logos.isMediafileUsedAsLogo(this); var mediafile = this;
return _.find(Logos.getAll(), function (logoPlaceholder) {
return logoPlaceholder.path === mediafile.mediafileUrl;
});
}, },
canBeUsedAsLogo: function () { canBeUsedAsLogo: function () {
return Logos.canMediafileBeUsedAsLogo(this); return this.is_image;
}, },
getLogos: function () { getLogos: function () {
return Logos.getLogosForMediafile(this); var mediafile = this;
return _.filter(Logos.getAll(), function (logoPlaceholder) {
return logoPlaceholder.path === mediafile.mediafileUrl;
});
},
hasLogo: function (logo) {
var allUrls = _.map(this.getLogos(), function (logo) {
return logo.path;
});
return _.includes(allUrls, logo.path);
},
toggleLogo: function (logo) {
if (this.hasLogo(logo)) {
Logos.set(logo.key);
} else {
Logos.set(logo.key, this.mediafileUrl);
}
},
isUsedAsFont: function () {
var mediafile = this;
return _.find(Fonts.getAll(), function (font) {
return font.path === mediafile.mediafileUrl;
});
},
canBeUsedAsFont: function () {
return this.is_font;
},
getFonts: function () {
var mediafile = this;
return _.filter(Fonts.getAll(), function (font) {
return font.path === mediafile.mediafileUrl;
});
},
hasFont: function (font) {
var allUrls = _.map(this.getFonts(), function (font) {
return font.path;
});
return _.includes(allUrls, font.path);
},
toggleFont: function (font) {
if (this.hasFont(font)) {
Fonts.set(font.key);
} else {
Fonts.set(font.key, this.mediafileUrl);
}
}, },
}, },
computed: { computed: {
@ -77,6 +125,11 @@ angular.module('OpenSlidesApp.mediafiles.resources', [
is_presentable: ['is_pdf', 'is_image', 'is_video', function (is_pdf, is_image, is_video) { is_presentable: ['is_pdf', 'is_image', 'is_video', function (is_pdf, is_image, is_video) {
return (is_pdf && !this.mediafile.encrypted) || is_image || is_video; return (is_pdf && !this.mediafile.encrypted) || is_image || is_video;
}], }],
is_font: [function () {
var FONT_FILE_EXTENSIONS = ['ttf', 'woff'];
var ext = _.last(this.mediafile.name.split('.'));
return _.includes(FONT_FILE_EXTENSIONS, ext);
}],
mediafileUrl: [function () { mediafileUrl: [function () {
return this.media_url_prefix + this.mediafile.name; return this.media_url_prefix + this.mediafile.name;
}], }],

View File

@ -291,8 +291,12 @@
<div class="icon-column"> <!-- horizontal block --> <div class="icon-column"> <!-- horizontal block -->
<i ng-style="{'visibility': mediafile.hidden ? 'visible' : 'hidden'}" class="fa fa-lock fa-lg" <i ng-style="{'visibility': mediafile.hidden ? 'visible' : 'hidden'}" class="fa fa-lock fa-lg"
title="{{ 'Is hidden' | translate }}"></i> title="{{ 'Is hidden' | translate }}"></i>
<i ng-style="{'visibility': mediafile.isUsedAsLogo() ? 'visible' : 'hidden'}" class="fa fa-picture-o fa-lg spacer-left" <span os-perms="core.can_manage_logos_and_fonts" class="spacer-left">
title="{{ 'Is used as a logo' | translate }}" os-perms="core.can_manage_logos"></i> <i ng-if="mediafile.isUsedAsLogo()" class="fa fa-picture-o fa-lg"
title="{{ 'Is used as a logo' | translate }}"></i>
<i ng-if="mediafile.isUsedAsFont()" class="fa fa-font fa-lg"
title="{{ 'Is used as a font' | translate }}"></i>
</span>
</div> </div>
<div class="title-column"> <div class="title-column">
<div> <!-- vertical block --> <div> <!-- vertical block -->
@ -325,7 +329,7 @@
</div> </div>
<div style="width: 40%;" class="pull-right optional"> <div style="width: 40%;" class="pull-right optional">
<!-- Logo placeholder dropdown for manage user --> <!-- Logo placeholder dropdown for manage user -->
<div os-perms="core.can_manage_logos" <div os-perms="core.can_manage_logos_and_fonts"
ng-mouseover="mediafile.logoHover=true" ng-mouseover="mediafile.logoHover=true"
ng-mouseleave="mediafile.logoHover=false" ng-mouseleave="mediafile.logoHover=false"
ng-show="mediafile.canBeUsedAsLogo()"> ng-show="mediafile.canBeUsedAsLogo()">
@ -354,14 +358,49 @@
</span> </span>
<ul class="dropdown-menu" aria-labelledby="dropdownLogos{{ mediafile.id }}"> <ul class="dropdown-menu" aria-labelledby="dropdownLogos{{ mediafile.id }}">
<li ng-repeat="logo in logos"> <li ng-repeat="logo in logos">
<a href ng-click="toggleLogo(mediafile, logo)"> <a href ng-click="mediafile.toggleLogo(logo)">
<i class="fa fa-check" ng-if="hasLogo(mediafile, logo)"></i> <i class="fa fa-check" ng-if="mediafile.hasLogo(logo)"></i>
{{ logo.display_name | translate }} {{ logo.display_name | translate }}
</a> </a>
</li> </li>
</ul> </ul>
</span> </span>
</div> </div>
<!-- Font placeholder dropdown for manage user -->
<div os-perms="core.can_manage_logos_and_fonts"
ng-mouseover="mediafile.fontHover=true"
ng-mouseleave="mediafile.fontHover=false"
ng-show="mediafile.canBeUsedAsFont()">
<span uib-dropdown>
<span id="dropdownFont{{ mediafile.id }}" class="pointer nobr" uib-dropdown-toggle>
<span uib-tooltip="{{ 'Manage fonts' | translate }}" tooltip-class="nobr">
<span ng-if="!mediafile.isUsedAsFont()" ng-show="mediafile.hover">
<i class="fa fa-font"></i>
<i class="fa fa-plus"></i>
</span>
<span ng-if="mediafile.isUsedAsFont()">
<span ng-repeat="font in mediafile.getFonts()">
<i class="fa fa-font spacer-right"
ng-style="{'visibility': $first ? 'visible' : 'hidden'}"></i>
<small>
{{ font.display_name | translate }}<span ng-if="!$last">,</br></span>
</small>
</span>
</span>
</span>
<i class="fa fa-cog fa-lg spacer-left" ng-show="mediafile.fontHover && mediafile.isUsedAsFont()"></i>
</span>
<ul class="dropdown-menu" aria-labelledby="dropdownFonts{{ mediafile.id }}">
<li ng-repeat="font in fonts">
<a href ng-click="mediafile.toggleFont(font)">
<i class="fa fa-check" ng-if="mediafile.hasFont(font)"></i>
{{ font.display_name | translate }}
</a>
</li>
</ul>
</span>
</div>
</div> </div>
</div> </div>

View File

@ -39,7 +39,7 @@ def create_builtin_groups_and_admin(**kwargs):
'assignments.can_nominate_self', 'assignments.can_nominate_self',
'assignments.can_see', 'assignments.can_see',
'core.can_manage_config', 'core.can_manage_config',
'core.can_manage_logos', 'core.can_manage_logos_and_fonts',
'core.can_manage_projector', 'core.can_manage_projector',
'core.can_manage_tags', 'core.can_manage_tags',
'core.can_manage_chat', 'core.can_manage_chat',
@ -116,7 +116,7 @@ def create_builtin_groups_and_admin(**kwargs):
permission_dict['core.can_see_frontpage'], permission_dict['core.can_see_frontpage'],
permission_dict['core.can_see_projector'], permission_dict['core.can_see_projector'],
permission_dict['core.can_manage_config'], permission_dict['core.can_manage_config'],
permission_dict['core.can_manage_logos'], permission_dict['core.can_manage_logos_and_fonts'],
permission_dict['core.can_manage_projector'], permission_dict['core.can_manage_projector'],
permission_dict['core.can_manage_tags'], permission_dict['core.can_manage_tags'],
permission_dict['core.can_use_chat'], permission_dict['core.can_use_chat'],