mirror of
https://github.com/Kunsi/pretalx-plugin-broadcast-tools
synced 2025-04-05 20:24:35 +00:00
Compare commits
151 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5613bf5acb | ||
![]() |
b07745b2ea | ||
![]() |
a05cbfc0d4 | ||
![]() |
1c951c6f65 | ||
![]() |
3af67e5e53 | ||
a3435e518f | |||
ade18a033e | |||
283686feb3 | |||
![]() |
baa8ce043c | ||
![]() |
de0c4721ed | ||
fec30e79ff | |||
![]() |
7fead51cb6 | ||
![]() |
65b8b87c8d | ||
6a6fc0be9e | |||
632c756245 | |||
929e41f0f8 | |||
0cc256d134 | |||
![]() |
3addc8b19e | ||
f63603d6de | |||
07090626e9 | |||
cb09f7c65a | |||
9d92add067 | |||
c0b3bdb55e | |||
f459c6c498 | |||
ed960358a4 | |||
dfa0945632 | |||
6a3b1b309e | |||
9ebcde7ab1 | |||
cd77ee8b91 | |||
73bd7f6c96 | |||
d1d3283c8e | |||
8dde19105d | |||
29db37bca5 | |||
410ca28b79 | |||
90c50c9652 | |||
bad650d5b9 | |||
443c7ce85a | |||
86a6075c30 | |||
620b2fb85e | |||
529f7f1eee | |||
3235a91ff5 | |||
d295ae18c3 | |||
![]() |
18cf1cd77a | ||
![]() |
d14f6e78f3 | ||
0b0df8b600 | |||
![]() |
74078695ed | ||
1a5aaa90fc | |||
513f3ebe75 | |||
![]() |
7c05357067 | ||
![]() |
c9e72db1f7 | ||
![]() |
545bdfb966 | ||
![]() |
6f8c536cb8 | ||
![]() |
d54087b7d0 | ||
![]() |
2e2efbff9d | ||
3917fa65cf | |||
81494bed54 | |||
819e8ea2aa | |||
77148e41f7 | |||
cb60e02671 | |||
25d8be0397 | |||
af4e3408db | |||
dabc8e5443 | |||
341a9c072c | |||
![]() |
98f8374b66 | ||
2b22f6a155 | |||
5335e911b7 | |||
![]() |
210a18f6b3 | ||
3063adcc73 | |||
8f8f8a90e2 | |||
433f316719 | |||
ca8a2e3c65 | |||
85893d9c9c | |||
c6ceedf041 | |||
1ad4c73ed4 | |||
![]() |
57351a4e1a | ||
![]() |
190ce8f222 | ||
3bd74850a5 | |||
0d3d96e96d | |||
d858f8d039 | |||
ab42dade2b | |||
6c058cca1f | |||
4c114d6cee | |||
de73a288b8 | |||
8b7acb10fb | |||
aa91af001c | |||
788273e870 | |||
0486cd44da | |||
46be2a02b5 | |||
3f7649b09a | |||
12864749f9 | |||
fa31e72db1 | |||
498782a962 | |||
c5cb67d969 | |||
737a72f296 | |||
bb55df2723 | |||
e0f47458cd | |||
08998e7535 | |||
6bc110b9fb | |||
04a7a0e3a9 | |||
c9b01acb6e | |||
c1604efb08 | |||
275935e747 | |||
7d5f278536 | |||
5eabb5fa6a | |||
078341e4ed | |||
11afb88602 | |||
97784373c0 | |||
16350548f5 | |||
de8065cf22 | |||
327981eade | |||
2da018775b | |||
d86cd011f3 | |||
dd0f0ad4f0 | |||
8dd9bec525 | |||
d42a2744d0 | |||
f618654ee0 | |||
40245ec86d | |||
d22f7826ac | |||
a272f21498 | |||
280458f6aa | |||
c9bfd7e4aa | |||
191ce772c5 | |||
2b02350e29 | |||
8cdb391dae | |||
b5f18033ba | |||
f41784175f | |||
1f0f928795 | |||
b9d0f42d98 | |||
76f615862f | |||
9634838c0f | |||
ac758f505e | |||
a4ef3f0da3 | |||
f18d59a368 | |||
1de7bcc749 | |||
31b6686279 | |||
c90b7c4fdc | |||
eb142b2483 | |||
bfbce9e98b | |||
5930907aa1 | |||
d03fef4e34 | |||
ddd4bca708 | |||
2e221cbf46 | |||
24acd451ad | |||
25fe8b804e | |||
c4ccb146ef | |||
![]() |
8ebed60b18 | ||
bc7f5414bd | |||
818b474bc4 | |||
![]() |
1c78eefb81 | ||
![]() |
d08e7db9bb | ||
4582e4db37 |
41 changed files with 2661 additions and 392 deletions
112
.github/workflows/style.yml
vendored
112
.github/workflows/style.yml
vendored
|
@ -18,14 +18,14 @@ jobs:
|
||||||
name: isort
|
name: isort
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: install gettext
|
- name: install gettext
|
||||||
run: sudo apt install gettext
|
run: sudo apt install gettext
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.12
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||||
|
@ -41,14 +41,14 @@ jobs:
|
||||||
name: flake8
|
name: flake8
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: install gettext
|
- name: install gettext
|
||||||
run: sudo apt install gettext
|
run: sudo apt install gettext
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.12
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||||
|
@ -65,14 +65,14 @@ jobs:
|
||||||
name: black
|
name: black
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: install gettext
|
- name: install gettext
|
||||||
run: sudo apt install gettext
|
run: sudo apt install gettext
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.12
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||||
|
@ -89,14 +89,14 @@ jobs:
|
||||||
name: docformatter
|
name: docformatter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: install gettext
|
- name: install gettext
|
||||||
run: sudo apt install gettext
|
run: sudo apt install gettext
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.12
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||||
|
@ -113,14 +113,14 @@ jobs:
|
||||||
name: djhtml
|
name: djhtml
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: install gettext
|
- name: install gettext
|
||||||
run: sudo apt install gettext
|
run: sudo apt install gettext
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.12
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||||
|
@ -133,34 +133,34 @@ jobs:
|
||||||
- name: Run docformatter
|
- name: Run docformatter
|
||||||
run: find -name "*.html" | xargs djhtml -c
|
run: find -name "*.html" | xargs djhtml -c
|
||||||
working-directory: .
|
working-directory: .
|
||||||
packaging:
|
# packaging:
|
||||||
name: packaging
|
# name: packaging
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v2
|
# - uses: actions/checkout@v4
|
||||||
- name: install gettext
|
# - name: install gettext
|
||||||
run: sudo apt install gettext
|
# run: sudo apt install gettext
|
||||||
- name: Set up Python 3.8
|
# - name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v1
|
# uses: actions/setup-python@v5
|
||||||
with:
|
# with:
|
||||||
python-version: 3.8
|
# python-version: 3.12
|
||||||
- uses: actions/cache@v1
|
# - uses: actions/cache@v4
|
||||||
with:
|
# with:
|
||||||
path: ~/.cache/pip
|
# path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
# key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||||
restore-keys: |
|
# restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
# ${{ runner.os }}-pip-
|
||||||
- name: Install pretalx
|
# - name: Install pretalx
|
||||||
run: pip3 install pretalx
|
# run: pip3 install pretalx
|
||||||
- name: Install Dependencies
|
# - name: Install Dependencies
|
||||||
run: pip3 install twine check-manifest -Ue .
|
# run: pip3 install twine check-manifest -Ue .
|
||||||
- name: Run check-manifest
|
# - name: Run check-manifest
|
||||||
run: check-manifest .
|
# run: check-manifest .
|
||||||
working-directory: .
|
# working-directory: .
|
||||||
- name: Build package
|
# - name: Build package
|
||||||
run: python setup.py sdist
|
# run: python setup.py sdist
|
||||||
working-directory: .
|
# working-directory: .
|
||||||
- name: Check package
|
# - name: Check package
|
||||||
run: twine check dist/*
|
# run: twine check dist/*
|
||||||
working-directory: .
|
# working-directory: .
|
||||||
|
#
|
||||||
|
|
58
CHANGELOG.md
Normal file
58
CHANGELOG.md
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# 2.2.0
|
||||||
|
|
||||||
|
* add plugin category (#16)
|
||||||
|
* add placeholders `{TRACK_NAME}` and `{TRACK_NAME_COLOURED}`
|
||||||
|
|
||||||
|
# 2.1.0
|
||||||
|
|
||||||
|
* fixed installation procedure
|
||||||
|
* add some more information to the json outputs to be able to be compatible
|
||||||
|
with [scheduled-plugin-pretalx-broadcast-tools](https://github.com/Kunsi/scheduled-plugin-pretalx-broadcast-tools)
|
||||||
|
(a plugin for [info-beamer hosted](https://info-beamer.com/))
|
||||||
|
|
||||||
|
# 2.0.1 (no longer available due to bugs during installation)
|
||||||
|
|
||||||
|
* fixes to support pretalx 2023.1.0
|
||||||
|
* use non-deprecated gettext call
|
||||||
|
* safe timezone handling
|
||||||
|
* usage of pyproject.toml
|
||||||
|
|
||||||
|
# 2.0.0
|
||||||
|
|
||||||
|
* room info page can now show more content on the lower half of the view
|
||||||
|
* **BREAKING:** The option to select which content should be shown
|
||||||
|
is now a ChoiceField, the old setting will be ignored.
|
||||||
|
* **BREAKING:** lower thirds now use css selectors using the same rules
|
||||||
|
as the other css selectors
|
||||||
|
* `#l3box` is now `#broadcast_tools_lower_thirds_box`
|
||||||
|
* `#l3info_line` is now `#broadcast_tools_lower_thirds_infoline`
|
||||||
|
* `#l3speaker` is now `#broadcast_tools_lower_thirds_speaker`
|
||||||
|
* `#l3title` is now `#broadcast_tools_lower_thirds_title`
|
||||||
|
* `.lower3rd` is now `broadcast_tools_lower_thirds`
|
||||||
|
|
||||||
|
# 1.1.0
|
||||||
|
|
||||||
|
* add a "room info" page to show conference attendees the currently running talk
|
||||||
|
* fix more compatibility issues with pretalx 2.3.x
|
||||||
|
|
||||||
|
# 1.0.4
|
||||||
|
|
||||||
|
* fix compatibility with pretalx 2.3.x
|
||||||
|
* always localize text using the selected default event locale
|
||||||
|
|
||||||
|
# 1.0.3
|
||||||
|
|
||||||
|
* fix a bug where questions could not be sorted
|
||||||
|
|
||||||
|
# 1.0.2
|
||||||
|
|
||||||
|
* fix compatibility issue with pretalx 2.3.1
|
||||||
|
|
||||||
|
# 1.0.1
|
||||||
|
|
||||||
|
* fix version identifier in setup.py
|
||||||
|
|
||||||
|
# 1.0.0
|
||||||
|
|
||||||
|
* PDF export: contains talk details, notes and answers to questions
|
||||||
|
* Lower Thirds: containing talk details to embed in a stream or recording
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright 2021 Franziska 'kunsi' Kunsmann
|
Copyright 2021-2024 Franziska 'kunsi' Kunsmann
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
include Makefile
|
include Makefile
|
||||||
|
include *.md
|
||||||
recursive-include img *.png
|
recursive-include img *.png
|
||||||
recursive-include pretalx_broadcast_tools *.py
|
recursive-include pretalx_broadcast_tools *.py
|
||||||
recursive-include pretalx_broadcast_tools/locale *
|
recursive-include pretalx_broadcast_tools/locale *
|
||||||
|
|
72
README.rst
72
README.rst
|
@ -1,30 +1,58 @@
|
||||||
Lower Thirds
|
Pretalx: Broadcast Tools (and more)
|
||||||
==========================
|
===================================
|
||||||
|
|
||||||
|
.. image:: http://translate.pretalx.com/widget/pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/svg-badge.svg
|
||||||
|
:alt: Translation status
|
||||||
|
:target: http://translate.pretalx.com/engage/pretalx-plugin-broadcast-tools/
|
||||||
|
|
||||||
This is a plugin for `pretalx`_.
|
This is a plugin for `pretalx`_.
|
||||||
|
|
||||||
This plugin allows you to add configurable lower thirds ("Bauchbinden"
|
This adds the following features to your pretalx instance:
|
||||||
in German) to your pretalx instance. Most likely this will be used in
|
|
||||||
(for example) a Browser Source inside `OBS Studio`_.
|
* Lower Thirds ("Bauchbinden") for using with something like OBS
|
||||||
|
* a "room info" screen, if you want to show information about the
|
||||||
|
currently running talk outside the room
|
||||||
|
* a pdf export containing information about a talk, so video helpers
|
||||||
|
can have easy access to the needed information
|
||||||
|
|
||||||
|
Screenshots
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The first two screenshots show the talk "Compatible static software" by
|
||||||
|
"Stephanie Fisher", which was generated by the `create_test_event` command.
|
||||||
|
The event color is `#EA652D`, a bright orange. The track color is `#857EB0`,
|
||||||
|
a light purple. The last screenshot shows "Multi-layered encompassing
|
||||||
|
paradigm" by "Michael Rodriguez".
|
||||||
|
|
||||||
.. image:: img/lower_thirds.png
|
.. image:: img/lower_thirds.png
|
||||||
:width: 400
|
:width: 400
|
||||||
:alt: Screenshot of the lower third output. There's currently a talk running.
|
:alt: Screenshot of "lower thirds" view. The box is located in the
|
||||||
|
bottom quarter of the screen, taking about half the screen width.
|
||||||
|
The box is mostly colored in the event color, with a small strip
|
||||||
|
showing the track color at the bottom. Inside the box the talk
|
||||||
|
title is shown in large text on top, the speaker name below that.
|
||||||
|
On the bottom right of the box the configured info line is shown.
|
||||||
|
|
||||||
The colours will be automatically determined from the event and track
|
.. image:: img/room_info.png
|
||||||
colours set inside pretalx.
|
:width: 400
|
||||||
|
:alt: Screenshot of the "room info" view. The whole screen is coloured
|
||||||
|
in the track color. On top of the screen you see the room name
|
||||||
|
in small font, below that the talk title in large letters. Below
|
||||||
|
that there's the speaker name listed. On the remainder of the
|
||||||
|
screen you see a large QR code linking to the talk detail page
|
||||||
|
in the schedule. The URL is also shown in plain-text below the
|
||||||
|
QR code.
|
||||||
|
|
||||||
You can also add a configurable third line to the lower thirds, for
|
.. image:: img/pdf_export.png
|
||||||
example to hint your audience to vote for the talks. To make this easier,
|
:width: 400
|
||||||
the plugin will automatically replace some placeholders inside the text,
|
:alt: Screenshot of the first half of a pdf page. On the top you see
|
||||||
so you can have individual text for all talks.
|
a large text "DO NOT RECORD - DO NOT STREAM", because the speaker
|
||||||
|
selected "Do not record" in the CfP. Below that you find
|
||||||
.. image:: img/orga_view.png
|
information like event- and room-name, date and time, talk title,
|
||||||
:width: 400
|
checkboxes for each speaker, length of the talk, talk abstract.
|
||||||
:alt: Screenshot of the orga view
|
Also you can find answers to select questions and notes entered
|
||||||
|
inside pretalx on the pdf export. In this screenshot most info
|
||||||
Inside the orga view, you can also configure the text snippet that's
|
has been extended by adding lots of "lorem ipsum" text.
|
||||||
shown if there's no talk running currently.
|
|
||||||
|
|
||||||
Development setup
|
Development setup
|
||||||
-----------------
|
-----------------
|
||||||
|
@ -35,7 +63,7 @@ Development setup
|
||||||
|
|
||||||
3. Activate the virtual environment you use for pretalx development.
|
3. Activate the virtual environment you use for pretalx development.
|
||||||
|
|
||||||
4. Execute ``python setup.py develop`` within this directory to register
|
4. Execute ``python -m pip install -e .`` within this directory to register
|
||||||
this application with pretalx's plugin registry.
|
this application with pretalx's plugin registry.
|
||||||
|
|
||||||
5. Execute ``make`` within this directory to compile translations.
|
5. Execute ``make`` within this directory to compile translations.
|
||||||
|
@ -48,7 +76,7 @@ Development setup
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Copyright 2021 Franziska 'kuns' Kunsmann
|
Copyright 2021-2023 Franziska 'kunsi' Kunsmann
|
||||||
|
|
||||||
Released under the terms of the Apache License 2.0
|
Released under the terms of the Apache License 2.0
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 54 KiB |
Binary file not shown.
Before Width: | Height: | Size: 87 KiB |
BIN
img/pdf_export.png
Normal file
BIN
img/pdf_export.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
BIN
img/room_info.png
Normal file
BIN
img/room_info.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
1
pretalx_broadcast_tools/__init__.py
Normal file
1
pretalx_broadcast_tools/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "2.4.0"
|
|
@ -1,6 +1,8 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
|
from pretalx_broadcast_tools import __version__
|
||||||
|
|
||||||
|
|
||||||
class PluginApp(AppConfig):
|
class PluginApp(AppConfig):
|
||||||
name = "pretalx_broadcast_tools"
|
name = "pretalx_broadcast_tools"
|
||||||
|
@ -15,7 +17,9 @@ class PluginApp(AppConfig):
|
||||||
"embedded into your broadcasting software"
|
"embedded into your broadcasting software"
|
||||||
)
|
)
|
||||||
visible = True
|
visible = True
|
||||||
version = "0.2.0"
|
version = __version__
|
||||||
|
category = "FEATURE"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signals # NOQA
|
from . import signals # NOQA
|
||||||
|
from . import tasks # NOQA
|
||||||
|
|
94
pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE
Normal file
94
pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
Copyright (c) 2009-2011 by Accademia di Belle Arti di Urbino and students
|
||||||
|
of MA course of Visual design. Some rights reserved.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
pretalx_broadcast_tools/assets/titilium-web-regular.ttf
Normal file
BIN
pretalx_broadcast_tools/assets/titilium-web-regular.ttf
Normal file
Binary file not shown.
376
pretalx_broadcast_tools/exporter.py
Normal file
376
pretalx_broadcast_tools/exporter.py
Normal file
|
@ -0,0 +1,376 @@
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_noop
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
from pretalx.schedule.exporters import ScheduleData
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.lib.enums import TA_CENTER
|
||||||
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
||||||
|
from reportlab.lib.units import mm
|
||||||
|
from reportlab.platypus import (
|
||||||
|
Flowable,
|
||||||
|
PageBreak,
|
||||||
|
Paragraph,
|
||||||
|
SimpleDocTemplate,
|
||||||
|
Spacer,
|
||||||
|
Table,
|
||||||
|
TableStyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pretalx_broadcast_tools.utils.placeholders import placeholders
|
||||||
|
|
||||||
|
A4_WIDTH, A4_HEIGHT = A4
|
||||||
|
PAGE_PADDING = 10 * mm
|
||||||
|
|
||||||
|
|
||||||
|
def _(text):
|
||||||
|
return LazyI18nString.from_gettext(gettext_noop(text))
|
||||||
|
|
||||||
|
|
||||||
|
class PDFInfoPage(Flowable):
|
||||||
|
def __init__(self, event, schedule, fahrplan_day, room_details, talk, style):
|
||||||
|
super().__init__()
|
||||||
|
self.event = event
|
||||||
|
self.schedule = schedule
|
||||||
|
self.day = fahrplan_day
|
||||||
|
self.room = room_details
|
||||||
|
self.talk = talk
|
||||||
|
self.style = style
|
||||||
|
self.y_position = PAGE_PADDING
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _questions(self):
|
||||||
|
return {
|
||||||
|
int(i.strip())
|
||||||
|
for i in (
|
||||||
|
self.event.settings.broadcast_tools_pdf_questions_to_include or ""
|
||||||
|
).split(",")
|
||||||
|
if i
|
||||||
|
}
|
||||||
|
|
||||||
|
def _add(self, item, gap=2):
|
||||||
|
_, height = item.wrapOn(
|
||||||
|
self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING
|
||||||
|
)
|
||||||
|
self.y_position += height + gap * mm
|
||||||
|
item.drawOn(self.canv, PAGE_PADDING, -self.y_position)
|
||||||
|
|
||||||
|
def _checkbox_text(self, text, **kwargs):
|
||||||
|
item = Paragraph(text, **kwargs)
|
||||||
|
_, height = item.wrapOn(
|
||||||
|
self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING
|
||||||
|
)
|
||||||
|
self.y_position += height + 2 * mm
|
||||||
|
item.drawOn(self.canv, PAGE_PADDING + 1.3 * height, -self.y_position)
|
||||||
|
self.canv.rect(PAGE_PADDING, -self.y_position, height * 0.8, height * 0.8)
|
||||||
|
|
||||||
|
def _question_text(self, question, answer, **kwargs):
|
||||||
|
item = Paragraph(question, **kwargs)
|
||||||
|
_, height = item.wrapOn(
|
||||||
|
self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING
|
||||||
|
)
|
||||||
|
self.y_position += height + 2 * mm
|
||||||
|
item.drawOn(self.canv, PAGE_PADDING, -self.y_position)
|
||||||
|
|
||||||
|
item = Paragraph(answer, **kwargs)
|
||||||
|
_, height = item.wrapOn(
|
||||||
|
self.canv, A4_WIDTH - 3 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING
|
||||||
|
)
|
||||||
|
self.y_position += height + 2 * mm
|
||||||
|
item.drawOn(self.canv, 2 * PAGE_PADDING, -self.y_position)
|
||||||
|
|
||||||
|
def _space(self):
|
||||||
|
self._add(Spacer(1, PAGE_PADDING / 2))
|
||||||
|
|
||||||
|
def _localize(self, translation):
|
||||||
|
return translation.localize(self.event.locale)
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
if hasattr(self.talk, "local_start"):
|
||||||
|
talk_start = self.talk.local_start
|
||||||
|
else:
|
||||||
|
talk_start = self.talk.start.astimezone(self.event.tz)
|
||||||
|
if hasattr(self.talk, "local_end"):
|
||||||
|
talk_end = self.talk.local_end
|
||||||
|
else:
|
||||||
|
talk_end = self.talk.end.astimezone(self.event.tz)
|
||||||
|
# add some information horizontally to the side of the page
|
||||||
|
self.canv.saveState()
|
||||||
|
self.canv.rotate(90)
|
||||||
|
self.canv.setFont("Helvetica", 12)
|
||||||
|
self.canv.drawString(
|
||||||
|
-(A4_HEIGHT - (PAGE_PADDING / 3)),
|
||||||
|
-(PAGE_PADDING / 3),
|
||||||
|
" | ".join(
|
||||||
|
[
|
||||||
|
self.talk.submission.code,
|
||||||
|
self.talk.submission.submission_type.name.localize(
|
||||||
|
self.event.locale
|
||||||
|
),
|
||||||
|
self.event.name.localize(self.event.locale),
|
||||||
|
talk_start.isoformat(),
|
||||||
|
f"Day {self.day['index']}",
|
||||||
|
self.room["name"].localize(self.event.locale),
|
||||||
|
f"Schedule {self.schedule.version}",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.canv.restoreState()
|
||||||
|
|
||||||
|
if self.talk.submission.do_not_record:
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
self._localize(_("DO NOT RECORD - DO NOT STREAM")),
|
||||||
|
style=self.style["Warning"],
|
||||||
|
),
|
||||||
|
gap=0,
|
||||||
|
)
|
||||||
|
self._space()
|
||||||
|
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
" | ".join(
|
||||||
|
[
|
||||||
|
self._localize(self.event.name),
|
||||||
|
self._localize(self.room["name"]),
|
||||||
|
talk_start.strftime("%F"),
|
||||||
|
f'{talk_start.strftime("%T")} - {talk_end.strftime("%T")}',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style=self.style["Meta"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add(
|
||||||
|
Paragraph(self.talk.submission.title, style=self.style["Title"]), gap=0
|
||||||
|
)
|
||||||
|
self._space()
|
||||||
|
|
||||||
|
for spk in self.talk.submission.speakers.all():
|
||||||
|
self._checkbox_text(
|
||||||
|
spk.get_display_name(),
|
||||||
|
style=self.style["Speaker"],
|
||||||
|
)
|
||||||
|
self._space()
|
||||||
|
|
||||||
|
if self.talk.submission.track:
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
f"{self._localize(_('Track:'))} {self._localize(self.talk.submission.track.name)}",
|
||||||
|
style=self.style["Meta"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add(
|
||||||
|
Table(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
self._localize(_("Duration")),
|
||||||
|
self._localize(_("Language")),
|
||||||
|
self._localize(_("Type")),
|
||||||
|
self._localize(_("Code")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.talk.export_duration,
|
||||||
|
self.talk.submission.content_locale,
|
||||||
|
self.talk.submission.submission_type.name,
|
||||||
|
self.talk.submission.code,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
colWidths=30 * mm,
|
||||||
|
style=TableStyle(
|
||||||
|
[
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
|
||||||
|
("BOX", (0, 0), (-1, -1), 0.25, colors.black),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.talk.submission.abstract:
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
self.talk.submission.abstract,
|
||||||
|
style=self.style["Abstract"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.event.settings.broadcast_tools_pdf_additional_content:
|
||||||
|
self._space()
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
self.event.settings.broadcast_tools_pdf_additional_content.format(
|
||||||
|
**placeholders(self.schedule.event, self.talk)
|
||||||
|
),
|
||||||
|
style=self.style["Meta"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.talk.submission.answers and self._questions:
|
||||||
|
self._space()
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
self._localize(_("Questions")),
|
||||||
|
style=self.style["Heading"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for answer in self.talk.submission.answers.order_by("question__position"):
|
||||||
|
if answer.question.id not in self._questions:
|
||||||
|
continue
|
||||||
|
self._question_text(
|
||||||
|
self._localize(answer.question.question),
|
||||||
|
answer.answer,
|
||||||
|
style=self.style["Question"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for spk in self.talk.submission.speakers.all():
|
||||||
|
for answer in spk.answers.order_by("question__position"):
|
||||||
|
if answer.question.id not in self._questions:
|
||||||
|
continue
|
||||||
|
self._question_text(
|
||||||
|
f"{self._localize(answer.question.question)} ({spk.get_display_name()})",
|
||||||
|
answer.answer,
|
||||||
|
style=self.style["Question"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.talk.submission.notes:
|
||||||
|
self._space()
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
self._localize(_("Notes")),
|
||||||
|
style=self.style["Heading"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for line in self.talk.submission.notes.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
line,
|
||||||
|
style=self.style["Notes"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.talk.submission.internal_notes
|
||||||
|
and self.event.settings.broadcast_tools_pdf_show_internal_notes
|
||||||
|
):
|
||||||
|
self._space()
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
self._localize(_("Internal Notes")),
|
||||||
|
style=self.style["Heading"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for line in self.talk.submission.internal_notes.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
self._add(
|
||||||
|
Paragraph(
|
||||||
|
line,
|
||||||
|
style=self.style["Notes"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PDFExporter(ScheduleData):
|
||||||
|
identifier = "broadcast_pdf"
|
||||||
|
verbose_name = "Broadcast Tools PDF"
|
||||||
|
public = False
|
||||||
|
show_qrcode = False
|
||||||
|
icon = "fa-file-pdf"
|
||||||
|
|
||||||
|
def _add_pages(self, doc):
|
||||||
|
style = self._style()
|
||||||
|
pages = []
|
||||||
|
for fahrplan_day in self.data:
|
||||||
|
for room_details in fahrplan_day["rooms"]:
|
||||||
|
for talk in room_details["talks"]:
|
||||||
|
if (
|
||||||
|
talk.submission.do_not_record
|
||||||
|
and self.event.settings.broadcast_tools_pdf_ignore_do_not_record
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
pages.append(
|
||||||
|
PDFInfoPage(
|
||||||
|
self.event,
|
||||||
|
self.schedule,
|
||||||
|
fahrplan_day,
|
||||||
|
room_details,
|
||||||
|
talk,
|
||||||
|
style,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pages.append(PageBreak())
|
||||||
|
return pages
|
||||||
|
|
||||||
|
def _style(self):
|
||||||
|
stylesheet = StyleSheet1()
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(name="Normal", fontName="Helvetica", fontSize=12, leading=14)
|
||||||
|
)
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(
|
||||||
|
name="Title", fontName="Helvetica-Bold", fontSize=20, leading=24
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(
|
||||||
|
name="Speaker", fontName="Helvetica-Oblique", fontSize=12, leading=14
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(name="Meta", fontName="Helvetica", fontSize=14, leading=16)
|
||||||
|
)
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(
|
||||||
|
name="Heading", fontName="Helvetica-Bold", fontSize=14, leading=16
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(
|
||||||
|
name="Question", fontName="Helvetica", fontSize=12, leading=14
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(
|
||||||
|
name="Abstract", fontName="Helvetica-Oblique", fontSize=10, leading=12
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(name="Notes", fontName="Helvetica", fontSize=12, leading=14)
|
||||||
|
)
|
||||||
|
stylesheet.add(
|
||||||
|
ParagraphStyle(
|
||||||
|
name="Warning",
|
||||||
|
fontName="Helvetica-Bold",
|
||||||
|
fontSize=20,
|
||||||
|
leading=24,
|
||||||
|
alignment=TA_CENTER,
|
||||||
|
textColor=colors.red,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return stylesheet
|
||||||
|
|
||||||
|
def render(self, *args, **kwargs):
|
||||||
|
with NamedTemporaryFile(suffix=".pdf") as f:
|
||||||
|
doc = SimpleDocTemplate(
|
||||||
|
f.name,
|
||||||
|
pagesize=A4,
|
||||||
|
rightMargin=0,
|
||||||
|
leftMargin=0,
|
||||||
|
topMargin=0,
|
||||||
|
bottomMargin=0,
|
||||||
|
)
|
||||||
|
doc.build(self._add_pages(doc))
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{self.event.slug}_broadcast_tools_{self.schedule.version}.pdf",
|
||||||
|
"application/pdf",
|
||||||
|
f.read(),
|
||||||
|
)
|
|
@ -1,20 +1,96 @@
|
||||||
|
from django.forms import BooleanField, CharField, ChoiceField, Textarea
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from hierarkey.forms import HierarkeyForm
|
from hierarkey.forms import HierarkeyForm
|
||||||
from i18nfield.forms import I18nFormField, I18nFormMixin, I18nTextInput
|
from i18nfield.forms import I18nFormField, I18nFormMixin, I18nTextInput
|
||||||
|
|
||||||
|
|
||||||
class BroadcastToolsSettingsForm(I18nFormMixin, HierarkeyForm):
|
class BroadcastToolsSettingsForm(I18nFormMixin, HierarkeyForm):
|
||||||
lower_thirds_no_talk_info = I18nFormField(
|
broadcast_tools_lower_thirds_no_talk_info = I18nFormField(
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Will be shown as talk title if there's currently no talk running."
|
"Will be shown as talk title if there's currently no talk running."
|
||||||
),
|
),
|
||||||
label=_('"no talk running" information'),
|
label=_('"No talk running" information'),
|
||||||
widget=I18nTextInput,
|
widget=I18nTextInput,
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
lower_thirds_info_string = I18nFormField(
|
broadcast_tools_lower_thirds_info_string = I18nFormField(
|
||||||
help_text=_("Will only be shown if there's a talk running."),
|
help_text=_(
|
||||||
label=_("info line"),
|
"Will only be shown if there's a talk running. You may use "
|
||||||
|
"the place holders mentioned below. The info line will be shown "
|
||||||
|
"on the bottom right side of the lower third. Setting this to an "
|
||||||
|
"empty string will hide the line entirely."
|
||||||
|
),
|
||||||
|
label=_("Info line"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=I18nTextInput,
|
widget=I18nTextInput,
|
||||||
)
|
)
|
||||||
|
broadcast_tools_lower_thirds_export_voctomix = BooleanField(
|
||||||
|
help_text=_(
|
||||||
|
"If checked, pretalx will periodically generate voctomix-compatible "
|
||||||
|
"lower thirds images and make them available as <code>.tar.gz</code>."
|
||||||
|
),
|
||||||
|
label=_("Generate voctomix lower thirds"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
broadcast_tools_room_info_lower_content = ChoiceField(
|
||||||
|
choices=(
|
||||||
|
("", "No lower content"),
|
||||||
|
("public_qr", "QR code linking to the 'talk detail' page"),
|
||||||
|
(
|
||||||
|
"feedback_qr",
|
||||||
|
"QR code linking to the feedback page of the currently running talk",
|
||||||
|
),
|
||||||
|
("talk_image", "session image uploaded by the speaker(s)"),
|
||||||
|
),
|
||||||
|
help_text=_(
|
||||||
|
"If a talk is running, the room info page will always show "
|
||||||
|
"the talk title and the list of speakers. The content below "
|
||||||
|
"is configurable here."
|
||||||
|
),
|
||||||
|
label=_("lower content"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
broadcast_tools_room_info_show_next_talk = BooleanField(
|
||||||
|
help_text=_(
|
||||||
|
"If no talk is running in the room, show the time and title "
|
||||||
|
"of the next talk in the room."
|
||||||
|
),
|
||||||
|
label=_("Show next talk"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
broadcast_tools_pdf_show_internal_notes = BooleanField(
|
||||||
|
help_text=_(
|
||||||
|
"If checked, the value of the 'internal notes' field in a "
|
||||||
|
"submission will get added to the pdf export."
|
||||||
|
),
|
||||||
|
label=_("Show internal notes in pdf export"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
broadcast_tools_pdf_ignore_do_not_record = BooleanField(
|
||||||
|
help_text=_(
|
||||||
|
"If checked, 'do not record' talks will not generate a page "
|
||||||
|
"in the pdf export."
|
||||||
|
),
|
||||||
|
label=_("Ignore 'do not record' talks when generating pdf"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
broadcast_tools_pdf_questions_to_include = CharField(
|
||||||
|
help_text=_(
|
||||||
|
"Comma-Separated list of question ids to include in pdf export. "
|
||||||
|
"If empty, no questions will get added."
|
||||||
|
),
|
||||||
|
label=_("Questions to include"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
broadcast_tools_pdf_additional_content = CharField(
|
||||||
|
help_text=_(
|
||||||
|
"Additional content to print onto the PDF export. "
|
||||||
|
"Will get printed as-is. You may use the place holders "
|
||||||
|
"mentioned below."
|
||||||
|
),
|
||||||
|
label=_("Additional text"),
|
||||||
|
required=False,
|
||||||
|
widget=Textarea,
|
||||||
|
)
|
||||||
|
|
274
pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
Normal file
274
pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
# pretalx-broadcast-tools
|
||||||
|
# Copyright (C) 2024 Franziska 'kunsi' Kunsmann
|
||||||
|
# This file is distributed under the same license as the pretalx-broadcast-tools package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 2.4.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-11-13 06:37+0100\n"
|
||||||
|
"PO-Revision-Date: 2024-11-13 05:41+0000\n"
|
||||||
|
"Last-Translator: Franziska <pretalx@kunsmann.eu>\n"
|
||||||
|
"Language-Team: German <http://translate.pretalx.com/projects/"
|
||||||
|
"pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/de/>\n"
|
||||||
|
"Language: de_DE\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
|
"X-Generator: Weblate 5.6.2\n"
|
||||||
|
|
||||||
|
#: apps.py:12
|
||||||
|
msgid "Broadcasting Tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps.py:15
|
||||||
|
msgid ""
|
||||||
|
"Some tools which can be used for supporting a broadcasting software, for "
|
||||||
|
"example a 'lower third' page which can be embedded into your broadcasting "
|
||||||
|
"software"
|
||||||
|
msgstr ""
|
||||||
|
"Einige Helfer, die zur Unterstützung von Broadcasting-Software benutzt "
|
||||||
|
"werden kann, zum Beispiel eine 'Bauchbinden'-Seite, die in deine "
|
||||||
|
"Broadcasting-Software eingebunden werden kann"
|
||||||
|
|
||||||
|
#: exporter.py:124
|
||||||
|
msgid "DO NOT RECORD - DO NOT STREAM"
|
||||||
|
msgstr "NICHT AUFNEHMEN - NICHT STREAMEN"
|
||||||
|
|
||||||
|
#: exporter.py:167
|
||||||
|
msgid "Duration"
|
||||||
|
msgstr "Dauer"
|
||||||
|
|
||||||
|
#: exporter.py:168
|
||||||
|
msgid "Language"
|
||||||
|
msgstr "Sprache"
|
||||||
|
|
||||||
|
#: exporter.py:169
|
||||||
|
msgid "Type"
|
||||||
|
msgstr "Typ"
|
||||||
|
|
||||||
|
#: exporter.py:170
|
||||||
|
msgid "Code"
|
||||||
|
msgstr "Code"
|
||||||
|
|
||||||
|
#: exporter.py:214
|
||||||
|
msgid "Questions"
|
||||||
|
msgstr "Fragen"
|
||||||
|
|
||||||
|
#: exporter.py:242
|
||||||
|
msgid "Notes"
|
||||||
|
msgstr "Notizen"
|
||||||
|
|
||||||
|
#: exporter.py:264
|
||||||
|
msgid "Internal Notes"
|
||||||
|
msgstr "Interne Notizen"
|
||||||
|
|
||||||
|
#: forms.py:10
|
||||||
|
msgid "Will be shown as talk title if there's currently no talk running."
|
||||||
|
msgstr "Wird angezeigt, wenn derzeit kein Vortrag läuft."
|
||||||
|
|
||||||
|
#: forms.py:12
|
||||||
|
msgid "\"No talk running\" information"
|
||||||
|
msgstr "\"Derzeit kein Vortrag\"-Informationen"
|
||||||
|
|
||||||
|
#: forms.py:18
|
||||||
|
msgid ""
|
||||||
|
"Will only be shown if there's a talk running. You may use the place holders "
|
||||||
|
"mentioned below. The info line will be shown on the bottom right side of the "
|
||||||
|
"lower third. Setting this to an empty string will hide the line entirely."
|
||||||
|
msgstr ""
|
||||||
|
"Wird nur angezeigt, wenn derzeit ein Vortrag läuft. Du kannst die oben "
|
||||||
|
"genannten Platzhalter benutzen. Die Info-Zeile wird unten rechts in den "
|
||||||
|
"Bauchbinden angezeigt. Wenn das Feld leer ist, wird die Zeile automatisch "
|
||||||
|
"ausgeblendet."
|
||||||
|
|
||||||
|
#: forms.py:23
|
||||||
|
msgid "Info line"
|
||||||
|
msgstr "Info-Zeile"
|
||||||
|
|
||||||
|
#: forms.py:29
|
||||||
|
msgid ""
|
||||||
|
"If checked, pretalx will periodically generate voctomix-compatible lower "
|
||||||
|
"thirds images and make them available as <code>.tar.gz</code>."
|
||||||
|
msgstr ""
|
||||||
|
"Wenn aktiviert, wird pretalx automatisch voctomix-kompatible bauchbinden "
|
||||||
|
"generieren und diese als <code>.tar.gz</code> zur Verfügung stellen."
|
||||||
|
|
||||||
|
#: forms.py:32
|
||||||
|
msgid "Generate voctomix lower thirds"
|
||||||
|
msgstr "Erzeuge voctomix-Bauchbinden"
|
||||||
|
|
||||||
|
#: forms.py:47
|
||||||
|
msgid ""
|
||||||
|
"If a talk is running, the room info page will always show the talk title and "
|
||||||
|
"the list of speakers. The content below is configurable here."
|
||||||
|
msgstr ""
|
||||||
|
"Wenn ein Vortrag läuft, zeigt die Raum-Info-Seite immer den Vortrags-Titel "
|
||||||
|
"und die Liste der Vortragenden an. Der Inhalt unterhalb dessen ist hier "
|
||||||
|
"konfigurierbar."
|
||||||
|
|
||||||
|
#: forms.py:51
|
||||||
|
msgid "lower content"
|
||||||
|
msgstr "Unterer Inhalt"
|
||||||
|
|
||||||
|
#: forms.py:56
|
||||||
|
msgid ""
|
||||||
|
"If no talk is running in the room, show the time and title of the next talk "
|
||||||
|
"in the room."
|
||||||
|
msgstr ""
|
||||||
|
"Wenn derzeit kein Vortrag läuft, soll die Startzeit und der Titel des "
|
||||||
|
"nächsten Vortrags angezeigt werden."
|
||||||
|
|
||||||
|
#: forms.py:59
|
||||||
|
msgid "Show next talk"
|
||||||
|
msgstr "Zeige nächsten Vortrag"
|
||||||
|
|
||||||
|
#: forms.py:65
|
||||||
|
msgid ""
|
||||||
|
"If checked, the value of the 'internal notes' field in a submission will get "
|
||||||
|
"added to the pdf export."
|
||||||
|
msgstr ""
|
||||||
|
"Wenn aktiviert, wird der Inhalt des Feldes 'Interne Notizen' im PDF-Export "
|
||||||
|
"angezeigt."
|
||||||
|
|
||||||
|
#: forms.py:68
|
||||||
|
msgid "Show internal notes in pdf export"
|
||||||
|
msgstr "Zeige interne Notizen im PDF-Export"
|
||||||
|
|
||||||
|
#: forms.py:73
|
||||||
|
msgid ""
|
||||||
|
"If checked, 'do not record' talks will not generate a page in the pdf export."
|
||||||
|
msgstr ""
|
||||||
|
"Wenn aktiviert, werden Vorträge mit 'Zeichnet meinen Vortrag nicht auf'-Flag "
|
||||||
|
"keine Seite im PDF-Export generieren."
|
||||||
|
|
||||||
|
#: forms.py:76
|
||||||
|
msgid "Ignore 'do not record' talks when generating pdf"
|
||||||
|
msgstr "Ignoriere 'Zeichnet meinen Vortrag nicht auf'-Vorträge im PDF-Export"
|
||||||
|
|
||||||
|
#: forms.py:81
|
||||||
|
msgid ""
|
||||||
|
"Comma-Separated list of question ids to include in pdf export. If empty, no "
|
||||||
|
"questions will get added."
|
||||||
|
msgstr ""
|
||||||
|
"Komma-separierte Liste von Fragen, die im PDF-Export eingebunden werden. "
|
||||||
|
"Falls dieses Feld leer ist, werden keine Fragen im PDF angezeigt."
|
||||||
|
|
||||||
|
#: forms.py:84
|
||||||
|
msgid "Questions to include"
|
||||||
|
msgstr "Eingebundene Fragen"
|
||||||
|
|
||||||
|
#: forms.py:89
|
||||||
|
msgid ""
|
||||||
|
"Additional content to print onto the PDF export. Will get printed as-is. You "
|
||||||
|
"may use the place holders mentioned below."
|
||||||
|
msgstr ""
|
||||||
|
"Zusätzlicher Inhalt, der auf dem PDF-Export angezeigt wird. Wird wie hier "
|
||||||
|
"eingegeben angezeigt. Du kannst die oben genannten Platzhalter benutzen."
|
||||||
|
|
||||||
|
#: forms.py:93
|
||||||
|
msgid "Additional text"
|
||||||
|
msgstr "Zusätzlicher Text"
|
||||||
|
|
||||||
|
#: signals.py:13
|
||||||
|
msgid "Sorry, there's currently no talk running"
|
||||||
|
msgstr "Leider läuft aktuell kein Vortrag"
|
||||||
|
|
||||||
|
#: signals.py:27
|
||||||
|
msgid "broadcast tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:8
|
||||||
|
msgid "broadcasting tools"
|
||||||
|
msgstr "Broadcasting-Tools"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:13
|
||||||
|
msgid "room"
|
||||||
|
msgstr "Raum"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:14
|
||||||
|
msgid "Feature"
|
||||||
|
msgstr "Funktion"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:21
|
||||||
|
msgid "Lower Thirds"
|
||||||
|
msgstr "Bauchbinden"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:22
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:63
|
||||||
|
msgid "Room Info"
|
||||||
|
msgstr "Raum-Information"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:23
|
||||||
|
msgid "Room Timer"
|
||||||
|
msgstr "Raum-Timer"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:28
|
||||||
|
msgid "Download voctomix-compatible lower thirds images"
|
||||||
|
msgstr "Lade voctomix-kompatible Bauchbinden-Bilder herunter"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:30
|
||||||
|
msgid "Placeholders"
|
||||||
|
msgstr "Platzhalter"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:31
|
||||||
|
msgid ""
|
||||||
|
"pretalx will automatically replace some placeholders in your custom content:"
|
||||||
|
msgstr "pretalx ersetzt automatisch einige Platzhalter in deinen Texten:"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:34
|
||||||
|
msgid ""
|
||||||
|
"talk code (<code>MUX9U3</code> for example) - most useful in combination "
|
||||||
|
"with pretalx-proposal-redirects or something like that"
|
||||||
|
msgstr ""
|
||||||
|
"Vortrags-Slug (zum Beispiel <code>MUX9U3</code>) - am hilfreichsten in "
|
||||||
|
"Kombination mit pretalx-proposal-redirects oder ähnlichen Plugins"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:37
|
||||||
|
msgid "The event slug"
|
||||||
|
msgstr "Der Event-Slug"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:40
|
||||||
|
msgid "URL to the talk feedback page."
|
||||||
|
msgstr "Adresse der Vortrags-Feedback-Seite"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:43
|
||||||
|
msgid "The talk slug"
|
||||||
|
msgstr "Der Vortrags-Slug"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:46
|
||||||
|
msgid "URL to the talk detail page."
|
||||||
|
msgstr "Adresse der Vortrags-Seite"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:48
|
||||||
|
msgid "or"
|
||||||
|
msgstr "oder"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:49
|
||||||
|
msgid "Track name in plain text or coloured using the track colour."
|
||||||
|
msgstr "Track-Name in einfachem Text oder in der Track-Farbe eingefärbt."
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:52
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr "Einstellungen"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:70
|
||||||
|
msgid "PDF Export"
|
||||||
|
msgstr "PDF-Export"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:86
|
||||||
|
msgid "Save"
|
||||||
|
msgstr "Speichern"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:55
|
||||||
|
msgid "Lower thirds"
|
||||||
|
msgstr "Bauchbinden"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:63
|
||||||
|
msgid "Room info"
|
||||||
|
msgstr "Raum-Information"
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:70
|
||||||
|
msgid "PDF export"
|
||||||
|
msgstr "PDF-Export"
|
240
pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
Normal file
240
pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-11-13 06:37+0100\n"
|
||||||
|
"PO-Revision-Date: 2025-02-05 12:56+0000\n"
|
||||||
|
"Last-Translator: Harrissou Sant-anna <delazj@gmail.com>\n"
|
||||||
|
"Language-Team: French <http://translate.pretalx.com/projects/"
|
||||||
|
"pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/fr/>\n"
|
||||||
|
"Language: fr_FR\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||||
|
"X-Generator: Weblate 5.6.2\n"
|
||||||
|
|
||||||
|
#: apps.py:12
|
||||||
|
msgid "Broadcasting Tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps.py:15
|
||||||
|
msgid ""
|
||||||
|
"Some tools which can be used for supporting a broadcasting software, for "
|
||||||
|
"example a 'lower third' page which can be embedded into your broadcasting "
|
||||||
|
"software"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: exporter.py:124
|
||||||
|
msgid "DO NOT RECORD - DO NOT STREAM"
|
||||||
|
msgstr "NE PAS ENREGISTRER - NE PAS DIFFUSER EN CONTINU"
|
||||||
|
|
||||||
|
#: exporter.py:167
|
||||||
|
msgid "Duration"
|
||||||
|
msgstr "Durée"
|
||||||
|
|
||||||
|
#: exporter.py:168
|
||||||
|
msgid "Language"
|
||||||
|
msgstr "Langue"
|
||||||
|
|
||||||
|
#: exporter.py:169
|
||||||
|
msgid "Type"
|
||||||
|
msgstr "Type"
|
||||||
|
|
||||||
|
#: exporter.py:170
|
||||||
|
msgid "Code"
|
||||||
|
msgstr "Code"
|
||||||
|
|
||||||
|
#: exporter.py:214
|
||||||
|
msgid "Questions"
|
||||||
|
msgstr "Questions"
|
||||||
|
|
||||||
|
#: exporter.py:242
|
||||||
|
msgid "Notes"
|
||||||
|
msgstr "Notes"
|
||||||
|
|
||||||
|
#: exporter.py:264
|
||||||
|
msgid "Internal Notes"
|
||||||
|
msgstr "Notes internes"
|
||||||
|
|
||||||
|
#: forms.py:10
|
||||||
|
msgid "Will be shown as talk title if there's currently no talk running."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:12
|
||||||
|
msgid "\"No talk running\" information"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:18
|
||||||
|
msgid ""
|
||||||
|
"Will only be shown if there's a talk running. You may use the place holders "
|
||||||
|
"mentioned below. The info line will be shown on the bottom right side of the "
|
||||||
|
"lower third. Setting this to an empty string will hide the line entirely."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:23
|
||||||
|
msgid "Info line"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:29
|
||||||
|
msgid ""
|
||||||
|
"If checked, pretalx will periodically generate voctomix-compatible lower "
|
||||||
|
"thirds images and make them available as <code>.tar.gz</code>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:32
|
||||||
|
msgid "Generate voctomix lower thirds"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:47
|
||||||
|
msgid ""
|
||||||
|
"If a talk is running, the room info page will always show the talk title and "
|
||||||
|
"the list of speakers. The content below is configurable here."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:51
|
||||||
|
msgid "lower content"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:56
|
||||||
|
msgid ""
|
||||||
|
"If no talk is running in the room, show the time and title of the next talk "
|
||||||
|
"in the room."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:59
|
||||||
|
msgid "Show next talk"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:65
|
||||||
|
msgid ""
|
||||||
|
"If checked, the value of the 'internal notes' field in a submission will get "
|
||||||
|
"added to the pdf export."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:68
|
||||||
|
msgid "Show internal notes in pdf export"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:73
|
||||||
|
msgid ""
|
||||||
|
"If checked, 'do not record' talks will not generate a page in the pdf export."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:76
|
||||||
|
msgid "Ignore 'do not record' talks when generating pdf"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:81
|
||||||
|
msgid ""
|
||||||
|
"Comma-Separated list of question ids to include in pdf export. If empty, no "
|
||||||
|
"questions will get added."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:84
|
||||||
|
msgid "Questions to include"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:89
|
||||||
|
msgid ""
|
||||||
|
"Additional content to print onto the PDF export. Will get printed as-is. You "
|
||||||
|
"may use the place holders mentioned below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:93
|
||||||
|
msgid "Additional text"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: signals.py:13
|
||||||
|
msgid "Sorry, there's currently no talk running"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: signals.py:27
|
||||||
|
msgid "broadcast tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:8
|
||||||
|
msgid "broadcasting tools"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:13
|
||||||
|
msgid "room"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:14
|
||||||
|
msgid "Feature"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:21
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:55
|
||||||
|
msgid "Lower Thirds"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:22
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:63
|
||||||
|
msgid "Room Info"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:23
|
||||||
|
msgid "Room Timer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:28
|
||||||
|
msgid "Download voctomix-compatible lower thirds images"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:30
|
||||||
|
msgid "Placeholders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:31
|
||||||
|
msgid ""
|
||||||
|
"pretalx will automatically replace some placeholders in your custom content:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:34
|
||||||
|
msgid ""
|
||||||
|
"talk code (<code>MUX9U3</code> for example) - most useful in combination "
|
||||||
|
"with pretalx-proposal-redirects or something like that"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:37
|
||||||
|
msgid "The event slug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:40
|
||||||
|
msgid "URL to the talk feedback page."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:43
|
||||||
|
msgid "The talk slug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:46
|
||||||
|
msgid "URL to the talk detail page."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:48
|
||||||
|
msgid "or"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:49
|
||||||
|
msgid "Track name in plain text or coloured using the track colour."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:52
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:70
|
||||||
|
msgid "PDF Export"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/pretalx_broadcast_tools/orga.html:86
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
|
@ -0,0 +1,343 @@
|
||||||
|
import logging
|
||||||
|
import tarfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django_scopes import scope, scopes_disabled
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from pretalx.agenda.management.commands.export_schedule_html import delete_directory
|
||||||
|
from pretalx.event.models import Event
|
||||||
|
|
||||||
|
from pretalx_broadcast_tools.utils.placeholders import placeholders
|
||||||
|
|
||||||
|
IMG_WIDTH = 1920 # px
|
||||||
|
IMG_HEIGHT = 1080 # px
|
||||||
|
|
||||||
|
FONT_SIZE_TITLE = 30 # px
|
||||||
|
FONT_SIZE_SPEAKER = 25 # px
|
||||||
|
FONT_SIZE_INFOLINE = 18 # px
|
||||||
|
FONT_FILE = (
|
||||||
|
Path(__file__).resolve().parent.parent.parent
|
||||||
|
/ "assets"
|
||||||
|
/ "titilium-web-regular.ttf"
|
||||||
|
)
|
||||||
|
|
||||||
|
BOX_PADDING = 10 # px
|
||||||
|
|
||||||
|
BOX_WIDTH = int(IMG_WIDTH * 0.8)
|
||||||
|
BOX_BOTTOM = int(IMG_HEIGHT - (IMG_HEIGHT * 0.1))
|
||||||
|
BOX_LEFT = int((IMG_WIDTH - BOX_WIDTH) / 2)
|
||||||
|
|
||||||
|
|
||||||
|
def get_export_path(event):
|
||||||
|
return settings.HTMLEXPORT_ROOT / event.slug / "broadcast-tools"
|
||||||
|
|
||||||
|
|
||||||
|
def get_export_targz_path(event):
|
||||||
|
return get_export_path(event).with_suffix(".voctomix.tar.gz")
|
||||||
|
|
||||||
|
|
||||||
|
def make_targz(generated_files, targz_path):
|
||||||
|
tmp_name = targz_path.with_suffix(".tmp")
|
||||||
|
tmp_name.unlink(missing_ok=True)
|
||||||
|
with tarfile.open(tmp_name, "w:gz") as tar:
|
||||||
|
for file in generated_files:
|
||||||
|
tar.add(file, arcname=file.name)
|
||||||
|
tmp_name.rename(targz_path)
|
||||||
|
|
||||||
|
|
||||||
|
class VoctomixLowerThirdsExporter:
|
||||||
|
def __init__(self, event, tmp_dir):
|
||||||
|
self.log = logging.getLogger(event.slug)
|
||||||
|
self.event = event
|
||||||
|
self.tmp_dir = tmp_dir
|
||||||
|
self.exported = set()
|
||||||
|
|
||||||
|
if event.primary_color:
|
||||||
|
self.primary_colour = self._hex2rgb(event.primary_color)
|
||||||
|
else:
|
||||||
|
# pretalx.settings.DEFAULT_EVENT_PRIMARY_COLOR
|
||||||
|
self.primary__color = (58, 165, 124)
|
||||||
|
|
||||||
|
self.infoline = event.settings.broadcast_tools_lower_thirds_info_string or ""
|
||||||
|
|
||||||
|
self.font_title = ImageFont.truetype(
|
||||||
|
FONT_FILE,
|
||||||
|
size=FONT_SIZE_TITLE,
|
||||||
|
encoding="unic",
|
||||||
|
)
|
||||||
|
self.font_speaker = ImageFont.truetype(
|
||||||
|
FONT_FILE,
|
||||||
|
size=FONT_SIZE_SPEAKER,
|
||||||
|
encoding="unic",
|
||||||
|
)
|
||||||
|
self.font_infoline = ImageFont.truetype(
|
||||||
|
FONT_FILE,
|
||||||
|
size=FONT_SIZE_INFOLINE,
|
||||||
|
encoding="unic",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _hex2rgb(self, hex_value):
|
||||||
|
hex_value = hex_value.lstrip("#")
|
||||||
|
# black insists this should have spaces around the :, but flake8
|
||||||
|
# complains about spaces around the :, soooooo ....
|
||||||
|
return tuple(int(hex_value[i : i + 2], 16) for i in (0, 2, 4)) # NOQA
|
||||||
|
|
||||||
|
def _fit_text(self, input_text, font, max_width):
|
||||||
|
words = [i.strip() for i in input_text.split()]
|
||||||
|
lines = []
|
||||||
|
line = []
|
||||||
|
for word in words:
|
||||||
|
new_line = " ".join([*line, word])
|
||||||
|
_, _, w, _ = font.getbbox(new_line)
|
||||||
|
if w > max_width:
|
||||||
|
# append old line to list of lines, then start new line with
|
||||||
|
# current word
|
||||||
|
lines.append(" ".join(line))
|
||||||
|
line = [word]
|
||||||
|
elif word.endswith(":"):
|
||||||
|
lines.append(new_line)
|
||||||
|
line = []
|
||||||
|
else:
|
||||||
|
line.append(word)
|
||||||
|
|
||||||
|
if line:
|
||||||
|
lines.append(" ".join(line))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def _get_infoline(self, talk):
|
||||||
|
infoline = self.infoline.localize(self.event.locale).format(
|
||||||
|
**placeholders(
|
||||||
|
self.event,
|
||||||
|
talk,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_, _, w, _ = self.font_infoline.getbbox(infoline)
|
||||||
|
return w, infoline
|
||||||
|
|
||||||
|
def export_speaker(self, talk, speaker):
|
||||||
|
img = Image.new(
|
||||||
|
mode="RGBA",
|
||||||
|
size=(IMG_WIDTH, IMG_HEIGHT),
|
||||||
|
color=(0, 0, 0, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
speaker_text = self._fit_text(
|
||||||
|
speaker.get_display_name(),
|
||||||
|
self.font_speaker,
|
||||||
|
BOX_WIDTH,
|
||||||
|
)
|
||||||
|
infoline_width, infoline_text = self._get_infoline(talk)
|
||||||
|
|
||||||
|
y_pos = BOX_BOTTOM - BOX_PADDING
|
||||||
|
if speaker_text:
|
||||||
|
y_pos -= len(speaker_text) * FONT_SIZE_SPEAKER
|
||||||
|
if infoline_text:
|
||||||
|
y_pos -= FONT_SIZE_INFOLINE
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
draw.rectangle(
|
||||||
|
[
|
||||||
|
(BOX_LEFT - BOX_PADDING, y_pos),
|
||||||
|
(BOX_LEFT + BOX_WIDTH + BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
|
||||||
|
],
|
||||||
|
fill=self.primary_colour,
|
||||||
|
)
|
||||||
|
if talk.submission.track and talk.submission.track.color:
|
||||||
|
draw.rectangle(
|
||||||
|
[
|
||||||
|
(BOX_LEFT - BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
|
||||||
|
(
|
||||||
|
BOX_LEFT + BOX_WIDTH + BOX_PADDING,
|
||||||
|
BOX_BOTTOM + (BOX_PADDING * 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fill=self._hex2rgb(talk.submission.track.color),
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in speaker_text:
|
||||||
|
draw.text(
|
||||||
|
(BOX_LEFT, y_pos),
|
||||||
|
line,
|
||||||
|
font=self.font_speaker,
|
||||||
|
fill=(255, 255, 255),
|
||||||
|
)
|
||||||
|
y_pos += FONT_SIZE_SPEAKER
|
||||||
|
|
||||||
|
if infoline_text:
|
||||||
|
draw.text(
|
||||||
|
(BOX_LEFT + BOX_WIDTH - infoline_width, y_pos),
|
||||||
|
infoline_text,
|
||||||
|
font=self.font_infoline,
|
||||||
|
fill=(255, 255, 255),
|
||||||
|
)
|
||||||
|
|
||||||
|
filename = self.tmp_dir / f"event_{talk.submission_id}_person_{speaker.id}.png"
|
||||||
|
img.save(filename)
|
||||||
|
self.log.debug(
|
||||||
|
f"Generated single-speaker image for {speaker.get_display_name()!r} "
|
||||||
|
"of talk {talk.submission.title!r}, saved as {filename}"
|
||||||
|
)
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def export_talk(self, talk):
|
||||||
|
img = Image.new(
|
||||||
|
mode="RGBA",
|
||||||
|
size=(IMG_WIDTH, IMG_HEIGHT),
|
||||||
|
color=(0, 0, 0, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
title_text = self._fit_text(talk.submission.title, self.font_title, BOX_WIDTH)
|
||||||
|
speaker_text = self._fit_text(
|
||||||
|
", ".join(
|
||||||
|
[person.get_display_name() for person in talk.submission.speakers.all()]
|
||||||
|
),
|
||||||
|
self.font_speaker,
|
||||||
|
BOX_WIDTH,
|
||||||
|
)
|
||||||
|
infoline_width, infoline_text = self._get_infoline(talk)
|
||||||
|
|
||||||
|
y_pos = BOX_BOTTOM - BOX_PADDING
|
||||||
|
if title_text:
|
||||||
|
y_pos -= len(title_text) * FONT_SIZE_TITLE
|
||||||
|
if speaker_text:
|
||||||
|
y_pos -= len(speaker_text) * FONT_SIZE_SPEAKER
|
||||||
|
if title_text and speaker_text:
|
||||||
|
y_pos -= BOX_PADDING
|
||||||
|
if infoline_text:
|
||||||
|
y_pos -= FONT_SIZE_INFOLINE
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
draw.rectangle(
|
||||||
|
[
|
||||||
|
(BOX_LEFT - BOX_PADDING, y_pos),
|
||||||
|
(BOX_LEFT + BOX_WIDTH + BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
|
||||||
|
],
|
||||||
|
fill=self.primary_colour,
|
||||||
|
)
|
||||||
|
if talk.submission.track and talk.submission.track.color:
|
||||||
|
draw.rectangle(
|
||||||
|
[
|
||||||
|
(BOX_LEFT - BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
|
||||||
|
(
|
||||||
|
BOX_LEFT + BOX_WIDTH + BOX_PADDING,
|
||||||
|
BOX_BOTTOM + (BOX_PADDING * 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fill=self._hex2rgb(talk.submission.track.color),
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in title_text:
|
||||||
|
draw.text(
|
||||||
|
(BOX_LEFT, y_pos),
|
||||||
|
line,
|
||||||
|
font=self.font_title,
|
||||||
|
fill=(255, 255, 255),
|
||||||
|
)
|
||||||
|
y_pos += FONT_SIZE_TITLE
|
||||||
|
|
||||||
|
if title_text and speaker_text:
|
||||||
|
y_pos += BOX_PADDING
|
||||||
|
|
||||||
|
for line in speaker_text:
|
||||||
|
draw.text(
|
||||||
|
(BOX_LEFT, y_pos),
|
||||||
|
line,
|
||||||
|
font=self.font_speaker,
|
||||||
|
fill=(255, 255, 255),
|
||||||
|
)
|
||||||
|
y_pos += FONT_SIZE_SPEAKER
|
||||||
|
|
||||||
|
if infoline_text:
|
||||||
|
draw.text(
|
||||||
|
(BOX_LEFT + BOX_WIDTH - infoline_width, y_pos),
|
||||||
|
infoline_text,
|
||||||
|
font=self.font_infoline,
|
||||||
|
fill=(255, 255, 255),
|
||||||
|
)
|
||||||
|
|
||||||
|
filename = self.tmp_dir / f"event_{talk.submission_id}_persons.png"
|
||||||
|
img.save(filename)
|
||||||
|
self.log.debug(
|
||||||
|
f"Generated image for talk {talk.submission.title!r}, saved as {filename}"
|
||||||
|
)
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
generated_files = set()
|
||||||
|
if not self.event.current_schedule:
|
||||||
|
raise CommandError(
|
||||||
|
f"event {self.event.slug} ({self.event.name}) does not have a schedule to be exported!"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log.info(
|
||||||
|
f"Generating voctomix-compatible lower thirds for event {self.event.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for talk in self.event.current_schedule.talks.filter(
|
||||||
|
is_visible=True
|
||||||
|
).select_related("submission"):
|
||||||
|
if talk.id in self.exported:
|
||||||
|
# account for talks that are scheduled multiple times
|
||||||
|
self.log.warning(
|
||||||
|
f"Talk {talk.submission.title!r} was already generated, skipping. "
|
||||||
|
"(Possibly scheduled multiple times?)"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if talk.submission is None:
|
||||||
|
self.log.info(
|
||||||
|
f"Talk {talk.id} has no associated submission, this is a break. "
|
||||||
|
"Skipping."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.log.info(f"Generating image(s) for talk {talk.submission.title!r}")
|
||||||
|
generated_files.add(self.export_talk(talk))
|
||||||
|
for speaker in talk.submission.speakers.all():
|
||||||
|
generated_files.add(self.export_speaker(talk, speaker))
|
||||||
|
self.exported.add(talk.id)
|
||||||
|
|
||||||
|
return generated_files
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
super().add_arguments(parser)
|
||||||
|
parser.add_argument("event", type=str)
|
||||||
|
parser.add_argument("--no-delete-source-files", action="store_true")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
event_slug = options.get("event")
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
try:
|
||||||
|
event = Event.objects.get(slug__iexact=event_slug)
|
||||||
|
except Event.DoesNotExist:
|
||||||
|
raise CommandError(f"could not find event with slug {event_slug!r}")
|
||||||
|
|
||||||
|
with scope(event=event):
|
||||||
|
logging.info(f"Exporting {event.name}")
|
||||||
|
|
||||||
|
export_dir = get_export_path(event)
|
||||||
|
targz_path = get_export_targz_path(event)
|
||||||
|
|
||||||
|
delete_directory(export_dir)
|
||||||
|
# for the first export of the conference, the "broadcast-tools"
|
||||||
|
# directory does not exist
|
||||||
|
export_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
exporter = VoctomixLowerThirdsExporter(event, export_dir)
|
||||||
|
generated_files = exporter.export()
|
||||||
|
make_targz(generated_files, targz_path)
|
||||||
|
except Exception:
|
||||||
|
logging.exception(f"Export of {event.name} failed")
|
||||||
|
else:
|
||||||
|
logging.info(
|
||||||
|
f"Export of {event.name} succeeded, export available at {targz_path}"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if not options.get("no_delete_source_files"):
|
||||||
|
delete_directory(export_dir)
|
|
@ -1,19 +1,20 @@
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import gettext_noop
|
from django.utils.translation import gettext_noop
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
from pretalx.common.models.settings import hierarkey
|
from pretalx.common.models.settings import hierarkey
|
||||||
|
from pretalx.common.signals import register_data_exporters
|
||||||
from pretalx.orga.signals import nav_event_settings
|
from pretalx.orga.signals import nav_event_settings
|
||||||
|
|
||||||
hierarkey.add_default(
|
hierarkey.add_default(
|
||||||
"lower_thirds_no_talk_info",
|
"broadcast_tools_lower_thirds_no_talk_info",
|
||||||
LazyI18nString.from_gettext(
|
LazyI18nString.from_gettext(
|
||||||
gettext_noop("Sorry, there's currently no talk running")
|
gettext_noop("Sorry, there's currently no talk running")
|
||||||
),
|
),
|
||||||
LazyI18nString,
|
LazyI18nString,
|
||||||
)
|
)
|
||||||
hierarkey.add_default("lower_thirds_info_string", "", LazyI18nString)
|
hierarkey.add_default("broadcast_tools_lower_thirds_info_string", "", LazyI18nString)
|
||||||
|
|
||||||
|
|
||||||
@receiver(nav_event_settings)
|
@receiver(nav_event_settings)
|
||||||
|
@ -23,7 +24,7 @@ def navbar_info(sender, request, **kwargs):
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"label": _("lower thirds"),
|
"label": _("broadcast tools"),
|
||||||
"url": reverse(
|
"url": reverse(
|
||||||
"plugins:pretalx_broadcast_tools:orga",
|
"plugins:pretalx_broadcast_tools:orga",
|
||||||
kwargs={
|
kwargs={
|
||||||
|
@ -34,3 +35,10 @@ def navbar_info(sender, request, **kwargs):
|
||||||
and url.url_name == "orga",
|
and url.url_name == "orga",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_data_exporters, dispatch_uid="exporter_broadcast_pdf")
|
||||||
|
def register_data_exporter(sender, **kwargs):
|
||||||
|
from .exporter import PDFExporter
|
||||||
|
|
||||||
|
return PDFExporter
|
||||||
|
|
|
@ -2,35 +2,124 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
|
color: white;
|
||||||
|
font-family: "Muli","Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#box {
|
body {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* room info *********************************************/
|
||||||
|
#broadcast_tools_room_info {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_info_header, #broadcast_tools_room_info_qr {
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_info_roomname {
|
||||||
|
font-size: 5vh;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_info_title {
|
||||||
|
font-size: 8vh;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_info_speaker {
|
||||||
|
font-size: 6vh;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_info_qr {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_info_qr img {
|
||||||
|
background-color: white;
|
||||||
|
height: calc(100% - 2vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_info_qr p {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* lower thirds ******************************************/
|
||||||
|
#broadcast_tools_lower_thirds_box {
|
||||||
width: 1020px;
|
width: 1020px;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-left: -510px;
|
margin-left: -510px;
|
||||||
|
|
||||||
color: white;
|
|
||||||
font-family: "Muli","Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
box-shadow: 5px 5px 10px 0px rgba(50, 50, 50, 0.75);
|
box-shadow: 5px 5px 10px 0px rgba(50, 50, 50, 0.75);
|
||||||
background-color: #3aa57c;
|
background-color: #3aa57c;
|
||||||
}
|
}
|
||||||
|
|
||||||
#title {
|
#broadcast_tools_lower_thirds_title {
|
||||||
font-size: 30px;
|
font-size: 3vh;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#speaker {
|
#broadcast_tools_lower_thirds_speaker {
|
||||||
font-size: 20px;
|
font-size: 2vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#info_line {
|
#broadcast_tools_lower_thirds_infoline {
|
||||||
font-size: 16px;
|
font-size: 1.8vh;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* room timer ********************************************/
|
||||||
|
#broadcast_tools_room_timer_header {
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_timer_title {
|
||||||
|
font-size: 8vh;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_timer_speaker {
|
||||||
|
font-size: 6vh;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_timer_scheduledata {
|
||||||
|
font-size: 3vh;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_timer_timeleft {
|
||||||
|
font-size: 6vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_timer_timeleft_timer {
|
||||||
|
font-size: 35vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_timer_progressbar, #broadcast_tools_room_timer_progressbar_bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#broadcast_tools_room_timer_progressbar {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
schedule = null;
|
||||||
|
event_info = null;
|
||||||
|
req = {};
|
||||||
|
|
||||||
|
function get_current_talk(max_offset) {
|
||||||
|
room_name = get_room_name();
|
||||||
|
|
||||||
|
if (!room_name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let offset = 0; offset <= max_offset; offset++) {
|
||||||
|
time_start = new Date(Date.now() + offset*60000).getTime();
|
||||||
|
time_end = new Date(Date.now() - offset*60000).getTime();
|
||||||
|
|
||||||
|
for (talk_i in schedule['talks']) {
|
||||||
|
talk = schedule['talks'][talk_i]
|
||||||
|
|
||||||
|
if (schedule['rooms'].length > 1 && talk['room'] != room_name) {
|
||||||
|
// not in this room
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
talk_start = new Date(talk['start']).getTime();
|
||||||
|
talk_end = new Date(talk['end']).getTime();
|
||||||
|
|
||||||
|
if (talk_start < time_start && talk_end > time_end) {
|
||||||
|
return talk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_next_talk() {
|
||||||
|
room_name = get_room_name();
|
||||||
|
|
||||||
|
if (!room_name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_start = new Date(Date.now()).getTime();
|
||||||
|
|
||||||
|
for (talk_i in schedule['talks']) {
|
||||||
|
talk = schedule['talks'][talk_i]
|
||||||
|
|
||||||
|
if (schedule['rooms'].length > 1 && talk['room'] != room_name) {
|
||||||
|
// not in this room
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
talk_start = new Date(talk['start']).getTime();
|
||||||
|
|
||||||
|
if (talk_start > time_start) {
|
||||||
|
return talk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_room_name() {
|
||||||
|
try {
|
||||||
|
hash = decodeURIComponent(window.location.hash.substring(1));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
if (event_info && event_info["rooms"].hasOwnProperty(hash)) {
|
||||||
|
return event_info["rooms"][hash];
|
||||||
|
}
|
||||||
|
// XXX remove fallback when releasing 3.0.0
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_time_from_pretalx(from_pretalx) {
|
||||||
|
d = new Date(from_pretalx);
|
||||||
|
|
||||||
|
h = d.getHours();
|
||||||
|
m = d.getMinutes();
|
||||||
|
|
||||||
|
if (h < 10) {
|
||||||
|
h = '0' + h;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m < 10) {
|
||||||
|
m = '0' + m;
|
||||||
|
}
|
||||||
|
|
||||||
|
return h + ':' + m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function xhr_get(url, callback_func) {
|
||||||
|
req[url] = new XMLHttpRequest();
|
||||||
|
req[url].timeout = 10000;
|
||||||
|
req[url].onreadystatechange = () => {
|
||||||
|
if (req[url].readyState === 4) {
|
||||||
|
if (req[url].status != 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback_func(req[url].responseText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req[url].open('GET', url);
|
||||||
|
req[url].setRequestHeader('Accept', 'application/json');
|
||||||
|
req[url].setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
||||||
|
req[url].send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_schedule() {
|
||||||
|
xhr_get('../event.json', function(text) {
|
||||||
|
console.debug("events: " + text);
|
||||||
|
event_info = JSON.parse(text);
|
||||||
|
});
|
||||||
|
xhr_get('../schedule.json', function(text) {
|
||||||
|
console.debug("schedule: " + text);
|
||||||
|
data = JSON.parse(text);
|
||||||
|
if ('error' in data) {
|
||||||
|
console.error(data['error']);
|
||||||
|
} else {
|
||||||
|
console.info('schedule updated with ' + data['talks'].length + ' talks in ' + data['rooms'].length + ' rooms');
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule = data;
|
||||||
|
|
||||||
|
window.setTimeout(update_schedule, 30000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
update_schedule();
|
|
@ -0,0 +1,52 @@
|
||||||
|
function update_lower_third() {
|
||||||
|
room_name = get_room_name();
|
||||||
|
|
||||||
|
if (!event_info) {
|
||||||
|
console.warn("Waiting for event info ...");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
box = document.getElementById('broadcast_tools_lower_thirds_box');
|
||||||
|
title = document.getElementById('broadcast_tools_lower_thirds_title');
|
||||||
|
speaker = document.getElementById('broadcast_tools_lower_thirds_speaker');
|
||||||
|
infoline = document.getElementById('broadcast_tools_lower_thirds_infoline');
|
||||||
|
|
||||||
|
box.style.backgroundColor = event_info['color'];
|
||||||
|
|
||||||
|
if (!schedule) {
|
||||||
|
title.innerHTML = 'Waiting for schedule ...';
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('error' in schedule) {
|
||||||
|
title.innerHTML = 'Error';
|
||||||
|
speaker.innerHTML = schedule['error'].join('<br>');
|
||||||
|
infoline.innerHTML = '';
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
|
||||||
|
title.innerHTML = 'Error';
|
||||||
|
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
|
||||||
|
infoline.innerHTML = '';
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
current_talk = get_current_talk(5);
|
||||||
|
if (current_talk) {
|
||||||
|
title.innerHTML = current_talk['title'];
|
||||||
|
speaker.innerHTML = current_talk['persons'].join(', ');
|
||||||
|
infoline.innerHTML = current_talk['infoline'];
|
||||||
|
} else {
|
||||||
|
title.innerHTML = event_info['no_talk'];
|
||||||
|
speaker.innerHTML = '';
|
||||||
|
infoline.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_talk && current_talk['track']) {
|
||||||
|
box.style.borderBottom = '10px solid ' + current_talk['track']['color'];
|
||||||
|
} else {
|
||||||
|
box.style.borderBottom = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.setInterval(update_lower_third, 1000);
|
|
@ -0,0 +1,81 @@
|
||||||
|
function update_room_info() {
|
||||||
|
room_name = get_room_name();
|
||||||
|
|
||||||
|
if (!event_info) {
|
||||||
|
console.warn("Waiting for event info ...");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
box = document.getElementById('broadcast_tools_room_info');
|
||||||
|
roomname = document.getElementById('broadcast_tools_room_info_roomname');
|
||||||
|
title = document.getElementById('broadcast_tools_room_info_title');
|
||||||
|
speaker = document.getElementById('broadcast_tools_room_info_speaker');
|
||||||
|
qr = document.getElementById('broadcast_tools_room_info_qr');
|
||||||
|
|
||||||
|
if (!room_name) {
|
||||||
|
roomname.innerHTML = event_info['name'];
|
||||||
|
title.innerHTML = 'Backstage';
|
||||||
|
speaker.innerHTML = '';
|
||||||
|
qr.innerHTML = '';
|
||||||
|
box.style.backgroundColor = event_info['color'];
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schedule) {
|
||||||
|
speaker.innerHTML = 'Waiting for schedule ...';
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('error' in schedule) {
|
||||||
|
title.innerHTML = 'Error';
|
||||||
|
speaker.innerHTML = schedule['error'].join('<br>');
|
||||||
|
qr.innerHTML = '';
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
|
||||||
|
title.innerHTML = 'Error';
|
||||||
|
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
|
||||||
|
qr.innerHTML = '';
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
current_talk = get_current_talk(15);
|
||||||
|
next_talk = get_next_talk();
|
||||||
|
|
||||||
|
if (current_talk) {
|
||||||
|
if (event_info['room-info']['lower_info'] == 'feedback_qr') {
|
||||||
|
qr_info = '<img src="' + current_talk['urls']['feedback_qr'] + '" alt="Feedback QR Code"><p>Leave Feedback by scanning the code or visiting ' + current_talk['urls']['feedback'] + '</p>';
|
||||||
|
} else if (event_info['room-info']['lower_info'] == 'public_qr') {
|
||||||
|
qr_info = '<img src="' + current_talk['urls']['public_qr'] + '" alt="QR Code linking to URL below"><p>' + current_talk['urls']['public'] + '</p>';
|
||||||
|
} else if (event_info['room-info']['lower_info'] == 'talk_image' && current_talk['image_url']) {
|
||||||
|
qr_info = '<img src="' + current_talk['image_url'] + '" alt="Talk image">';
|
||||||
|
} else {
|
||||||
|
qr_info = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
roomname.innerHTML = room_name;
|
||||||
|
title.innerHTML = current_talk['title'];
|
||||||
|
speaker.innerHTML = current_talk['persons'].join(', ');
|
||||||
|
qr.innerHTML = qr_info;
|
||||||
|
} else {
|
||||||
|
roomname.innerHTML = event_info['name'];
|
||||||
|
title.innerHTML = room_name;
|
||||||
|
qr.innerHTML = '';
|
||||||
|
|
||||||
|
if (next_talk && event_info['room-info']['show_next_talk']) {
|
||||||
|
speaker.innerHTML = format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title'];
|
||||||
|
} else {
|
||||||
|
speaker.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_talk && current_talk['track']) {
|
||||||
|
box.style.backgroundColor = current_talk['track']['color'];
|
||||||
|
} else if (next_talk && next_talk['track'] && event_info['room-info']['show_next_talk']) {
|
||||||
|
box.style.backgroundColor = next_talk['track']['color'];
|
||||||
|
} else {
|
||||||
|
box.style.backgroundColor = event_info['color'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.setInterval(update_room_info, 1000);
|
|
@ -0,0 +1,115 @@
|
||||||
|
function _left_zero_pad(i) {
|
||||||
|
if (i < 10) {
|
||||||
|
i = "0" + i;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_room_info() {
|
||||||
|
room_name = get_room_name();
|
||||||
|
|
||||||
|
if (!event_info) {
|
||||||
|
console.warn("Waiting for event info ...");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
box = document.getElementById('broadcast_tools_room_timer');
|
||||||
|
header = document.getElementById('broadcast_tools_room_timer_header');
|
||||||
|
title = document.getElementById('broadcast_tools_room_timer_title');
|
||||||
|
speaker = document.getElementById('broadcast_tools_room_timer_speaker');
|
||||||
|
scheduledata = document.getElementById('broadcast_tools_room_timer_scheduledata');
|
||||||
|
timeleft = document.getElementById('broadcast_tools_room_timer_timeleft_timer');
|
||||||
|
timehint = document.getElementById('broadcast_tools_room_timer_timeleft_hint');
|
||||||
|
progressbar= document.getElementById('broadcast_tools_room_timer_progressbar');
|
||||||
|
progressbar_bar = document.getElementById('broadcast_tools_room_timer_progressbar_bar');
|
||||||
|
|
||||||
|
box.style.backgroundColor = event_info['color'];
|
||||||
|
|
||||||
|
if (!schedule) {
|
||||||
|
speaker.innerHTML = 'Waiting for schedule ...';
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('error' in schedule) {
|
||||||
|
title.innerHTML = 'Error';
|
||||||
|
speaker.innerHTML = schedule['error'].join('<br>');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
|
||||||
|
title.innerHTML = 'Error';
|
||||||
|
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
current_talk = get_current_talk(60);
|
||||||
|
next_talk = get_next_talk();
|
||||||
|
now = new Date();
|
||||||
|
|
||||||
|
if (current_talk) {
|
||||||
|
title.innerHTML = current_talk['title'];
|
||||||
|
speaker.innerHTML = current_talk['persons'].join(', ');
|
||||||
|
scheduledata.innerHTML = format_time_from_pretalx(current_talk['start']);
|
||||||
|
scheduledata.innerHTML += ' - ';
|
||||||
|
scheduledata.innerHTML += format_time_from_pretalx(current_talk['end']);
|
||||||
|
|
||||||
|
scheduled_start = new Date(current_talk['start']);
|
||||||
|
scheduled_end = new Date(current_talk['end']);
|
||||||
|
|
||||||
|
if (scheduled_start > now) {
|
||||||
|
timeleft.innerHTML = '';
|
||||||
|
progressbar_bar.style.width = '0';
|
||||||
|
timehint.innerHTML = '';
|
||||||
|
} else if (scheduled_end < now) {
|
||||||
|
timeleft.innerHTML = '0:00';
|
||||||
|
progressbar_bar.style.width = '100vw';
|
||||||
|
timehint.innerHTML = 'talk has ended';
|
||||||
|
} else {
|
||||||
|
diff = scheduled_end - now;
|
||||||
|
let diff_s = Math.floor(diff / 1000) % 60;
|
||||||
|
let diff_m = Math.floor(diff / 1000 / 60) % 60;
|
||||||
|
let diff_h = Math.floor(diff / 1000 / 60 / 60);
|
||||||
|
|
||||||
|
if (diff_h > 0) {
|
||||||
|
timeleft.innerHTML = diff_h + ":" + _left_zero_pad(diff_m) + ":" + _left_zero_pad(diff_s);
|
||||||
|
} else {
|
||||||
|
timeleft.innerHTML = diff_m + ":" + _left_zero_pad(diff_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
total_time = scheduled_end - scheduled_start;
|
||||||
|
progressbar_bar.style.width = (((diff/total_time)*100)-100)*-1 + 'vw';
|
||||||
|
timehint.innerHTML = 'left in this talk';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_talk['track']) {
|
||||||
|
header.style.backgroundColor = current_talk['track']['color'];
|
||||||
|
progressbar.style.borderTop = '2px solid ' + current_talk['track']['color'];
|
||||||
|
progressbar_bar.style.backgroundColor = current_talk['track']['color'];
|
||||||
|
} else {
|
||||||
|
header.style.backgroundColor = null;
|
||||||
|
progressbar.style.borderTop = '2px solid white';
|
||||||
|
progressbar_bar.style.backgroundColor = 'white';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
progressbar.style.borderTop = 'none';
|
||||||
|
progressbar_bar.style.width = '0';
|
||||||
|
speaker.innerHTML = 'Break';
|
||||||
|
timehint.innerHTML = '';
|
||||||
|
title.innerHTML = room_name;
|
||||||
|
|
||||||
|
timeleft.innerHTML = _left_zero_pad(now.getHours()) + ":" + _left_zero_pad(now.getMinutes()) + ":" + _left_zero_pad(now.getSeconds());
|
||||||
|
|
||||||
|
if (next_talk) {
|
||||||
|
scheduledata.innerHTML = format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title'];
|
||||||
|
|
||||||
|
if (next_talk['track']) {
|
||||||
|
header.style.backgroundColor = next_talk['track']['color'];
|
||||||
|
} else {
|
||||||
|
header.style.backgroundColor = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scheduledata.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.setInterval(update_room_info, 1000);
|
|
@ -1,82 +0,0 @@
|
||||||
schedule = null;
|
|
||||||
room_name = null;
|
|
||||||
event_info = null;
|
|
||||||
|
|
||||||
$(function() {
|
|
||||||
$('#speaker').text('Content will appear soon.');
|
|
||||||
});
|
|
||||||
|
|
||||||
function update_lower_third() {
|
|
||||||
current_time = new Date(Date.now()).getTime()
|
|
||||||
|
|
||||||
try {
|
|
||||||
hash = decodeURIComponent(window.location.hash.substring(1));
|
|
||||||
room_name = hash;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!schedule || !event_info) {
|
|
||||||
console.warn("There's no schedule or no event info yet, exiting ...");
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
|
|
||||||
$('#title').text('Error')
|
|
||||||
$('#speaker').text('Invalid room_name. Valid names: ' + schedule['rooms'].join(', '));
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
current_talk = null;
|
|
||||||
|
|
||||||
for (talk_i in schedule['talks']) {
|
|
||||||
talk = schedule['talks'][talk_i]
|
|
||||||
|
|
||||||
if (schedule['rooms'].length > 1 && talk['room'] != room_name) {
|
|
||||||
// not in this room
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
talk_start = new Date(talk['start']).getTime();
|
|
||||||
talk_end = new Date(talk['end']).getTime();
|
|
||||||
|
|
||||||
if (talk_start < current_time && talk_end > current_time) {
|
|
||||||
current_talk = talk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current_talk) {
|
|
||||||
$('#title').text(current_talk['title']);
|
|
||||||
$('#speaker').text(current_talk['persons'].join(', '));
|
|
||||||
$('#info_line').text(current_talk['infoline']);
|
|
||||||
} else {
|
|
||||||
$('#title').text(event_info['no_talk']);
|
|
||||||
$('#speaker').text('');
|
|
||||||
$('#info_line').text('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current_talk && current_talk['track']) {
|
|
||||||
$('#box').css('border-bottom', '10px solid ' + current_talk['track']['color']);
|
|
||||||
} else {
|
|
||||||
$('#box').css('border-bottom', 'none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.setInterval(update_lower_third, 1000);
|
|
||||||
|
|
||||||
function update_schedule() {
|
|
||||||
$.getJSON('../event.json', function(data) {
|
|
||||||
event_info = data;
|
|
||||||
|
|
||||||
$('#box').css('background-color', data['color']);
|
|
||||||
});
|
|
||||||
$.getJSON('../schedule.json', function(data) {
|
|
||||||
console.info('schedule updated with ' + data['talks'].length + ' talks in ' + data['rooms'].length + ' rooms');
|
|
||||||
|
|
||||||
schedule = data;
|
|
||||||
|
|
||||||
window.setTimeout(update_schedule, 30000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
update_schedule();
|
|
78
pretalx_broadcast_tools/tasks.py
Normal file
78
pretalx_broadcast_tools/tasks.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scope, scopes_disabled
|
||||||
|
from pretalx.celery_app import app
|
||||||
|
from pretalx.common.signals import periodic_task
|
||||||
|
from pretalx.event.models import Event
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(name="pretalx_broadcast_tools.export_voctomix_lower_thirds")
|
||||||
|
def export_voctomix_lower_thirds(*, event_id):
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
event = Event.objects.filter(pk=event_id).first()
|
||||||
|
if not event:
|
||||||
|
LOG.error(f"Could not find event {event_id=} for export")
|
||||||
|
return
|
||||||
|
|
||||||
|
with scope(event=event):
|
||||||
|
if not event.current_schedule:
|
||||||
|
LOG.error(f"event {event.slug} does not have schedule, can't export")
|
||||||
|
return
|
||||||
|
|
||||||
|
call_command(
|
||||||
|
"export_voctomix_lower_thirds",
|
||||||
|
event.slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(name="pretalx_broadcast_tools.periodic_voctomix_export")
|
||||||
|
def task_periodic_voctomix_export(*, event_slug):
|
||||||
|
from pretalx_broadcast_tools.management.commands.export_voctomix_lower_thirds import (
|
||||||
|
get_export_targz_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
event = Event.objects.filter(slug=event_slug).first()
|
||||||
|
|
||||||
|
with scope(event=event):
|
||||||
|
if (
|
||||||
|
not event.settings.broadcast_tools_lower_thirds_export_voctomix
|
||||||
|
or not event.current_schedule
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
targz_path = get_export_targz_path(event)
|
||||||
|
needs_rebuild = False
|
||||||
|
last_rebuild = event.cache.get("broadcast_tools_last_voctomix_export")
|
||||||
|
_now = now()
|
||||||
|
if not targz_path.exists():
|
||||||
|
needs_rebuild = True
|
||||||
|
if not last_rebuild or _now - last_rebuild >= timedelta(hours=1):
|
||||||
|
needs_rebuild = True
|
||||||
|
if event.cache.get("broadcast_tools_force_new_voctomix_export"):
|
||||||
|
needs_rebuild = True
|
||||||
|
|
||||||
|
if needs_rebuild:
|
||||||
|
event.cache.delete("broadcast_tools_force_new_voctomix_export")
|
||||||
|
event.cache.set("broadcast_tools_last_voctomix_export", _now, None)
|
||||||
|
export_voctomix_lower_thirds.apply_async(
|
||||||
|
kwargs={"event_id": event.id}, ignore_result=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(periodic_task)
|
||||||
|
def periodic_event_services(sender, **kwargs):
|
||||||
|
for event in Event.objects.all():
|
||||||
|
with scope(event=event):
|
||||||
|
if (event.date_to + timedelta(days=2)) < now().date():
|
||||||
|
continue
|
||||||
|
task_periodic_voctomix_export.apply_async(
|
||||||
|
kwargs={"event_slug": event.slug}, ignore_result=True
|
||||||
|
)
|
|
@ -4,18 +4,20 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
|
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>{{ request.event.name }} lower thirds</title>
|
<title>{{ request.event.name }} lower thirds</title>
|
||||||
{% compress js %}
|
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
|
||||||
<script src="{% static "vendored/jquery-3.1.1.js" %}"></script>
|
<script src="{% static "pretalx_broadcast_tools/lower_thirds.js" %}"></script>
|
||||||
{% endcompress %}
|
|
||||||
<script src="{% static "pretalx_broadcast_tools/update.js" %}"></script>
|
|
||||||
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />
|
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />
|
||||||
|
{% if request.event and request.event.custom_css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ request.event.custom_css.url }}"/>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body id="broadcast_tools_lower_thirds">
|
||||||
<div id="box">
|
<div id="broadcast_tools_lower_thirds_box">
|
||||||
<p id="title">Loading ...</p>
|
<p id="broadcast_tools_lower_thirds_title">Loading ...</p>
|
||||||
<p id="speaker">Content should appear soon. If not, please verify you have Javascript enabled.</p>
|
<p id="broadcast_tools_lower_thirds_speaker">Content should appear soon. If not, please verify you have Javascript enabled.</p>
|
||||||
<p id="info_line"></p>
|
<p id="broadcast_tools_lower_thirds_infoline"></p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,40 +1,78 @@
|
||||||
{% extends "orga/base.html" %}
|
{% extends "orga/base.html" %}
|
||||||
{% load bootstrap4 %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<h2>{% translate "broadcasting tools" %}</h2>
|
||||||
|
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% translate "room" %}</th>
|
||||||
|
<th scope="col" colspan="3">{% translate "Feature" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for room in request.event.rooms.all %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ room.name }}</th>
|
||||||
|
<td><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds' request.event.slug %}#{{ room.uuid }}">{% translate "Lower Thirds" %}</a></td>
|
||||||
|
<td><a href="{% url 'plugins:pretalx_broadcast_tools:room_info' request.event.slug %}#{{ room.uuid }}">{% translate "Room Info" %}</a></td>
|
||||||
|
<td><a href="{% url 'plugins:pretalx_broadcast_tools:room_timer' request.event.slug %}#{{ room.uuid }}">{% translate "Room Timer" %}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds_voctomix_download' request.event.slug %}">{% translate "Download voctomix-compatible lower thirds images" %}</a></p>
|
||||||
|
|
||||||
|
<h2>{% translate "Placeholders" %}</h2>
|
||||||
|
<p>{% translate "pretalx will automatically replace some placeholders in your custom content:" %}</p>
|
||||||
|
<dl>
|
||||||
|
<dt><code>{CODE}</code></dt>
|
||||||
|
<dd>{% translate "talk code (<code>MUX9U3</code> for example) - most useful in combination with pretalx-proposal-redirects or something like that" %}</dd>
|
||||||
|
|
||||||
|
<dt><code>{EVENT_SLUG}</code></dt>
|
||||||
|
<dd>{% translate "The event slug" %} (<code>{{ request.event.slug }}</code>)</dd>
|
||||||
|
|
||||||
|
<dt><code>{FEEDBACK_URL}</code></dt>
|
||||||
|
<dd>{% translate "URL to the talk feedback page." %}</dd>
|
||||||
|
|
||||||
|
<dt><code>{TALK_SLUG}</code></dt>
|
||||||
|
<dd>{% translate "The talk slug" %} (<code>{{ request.event.slug }}-1-my-super-great-talk</code>)</dd>
|
||||||
|
|
||||||
|
<dt><code>{TALK_URL}</code></dt>
|
||||||
|
<dd>{% translate "URL to the talk detail page." %}</dd>
|
||||||
|
|
||||||
|
<dt><code>{TRACK_NAME}</code> {% translate "or" %} <code>{TRACK_NAME_COLOURED}</code></dt>
|
||||||
|
<dd>{% translate "Track name in plain text or coloured using the track colour." %}</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h2>{% translate "Settings" %}</h2>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>
|
<legend>
|
||||||
{% translate "Set up lower thirds" %}
|
{% translate "Lower Thirds" %}
|
||||||
</legend>
|
</legend>
|
||||||
{% bootstrap_field form.lower_thirds_no_talk_info layout='event' %}
|
{{ form.broadcast_tools_lower_thirds_no_talk_info.as_field_group }}
|
||||||
{% bootstrap_field form.lower_thirds_info_string layout='event' %}
|
{{ form.broadcast_tools_lower_thirds_info_string.as_field_group }}
|
||||||
<p>
|
{{ form.broadcast_tools_lower_thirds_export_voctomix.as_field_group }}
|
||||||
The info line will be shown on the bottom right side of your
|
</fieldset>
|
||||||
lower third. If you set it to an empty string, it will automatically
|
<fieldset>
|
||||||
hide itself.
|
<legend>
|
||||||
</p>
|
{% translate "Room Info" %}
|
||||||
<p>
|
</legend>
|
||||||
pretalx will automatically replace some placeholders in your info
|
{{ form.broadcast_tools_room_info_lower_content.as_field_group }}
|
||||||
string.
|
{{ form.broadcast_tools_room_info_show_next_talk.as_field_group }}
|
||||||
Use <code>{CODE}</code> to embed the talk code (<code>MUX9U3</code>
|
</fieldset>
|
||||||
for example). You could use this to directly link to the talk
|
<fieldset>
|
||||||
feedback page.
|
<legend>
|
||||||
Use <code>{EVENT_SLUG}</code> to get the event slug.
|
{% translate "PDF Export" %}
|
||||||
Use <code>{TALK_SLUG}</code> to get the talk slug.
|
</legend>
|
||||||
</p>
|
{{ form.broadcast_tools_pdf_show_internal_notes.as_field_group }}
|
||||||
|
{{ form.broadcast_tools_pdf_ignore_do_not_record.as_field_group }}
|
||||||
{% if request.event.rooms %}
|
{{ form.broadcast_tools_pdf_questions_to_include.as_field_group }}
|
||||||
<h3>{% trans "room list" %}</h3>
|
{{ form.broadcast_tools_pdf_additional_content.as_field_group }}
|
||||||
<ul>
|
|
||||||
{% for room in request.event.rooms.all %}
|
|
||||||
<li><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds' request.event.slug %}#{{ room.name }}">{{ room.name }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="submit-group panel">
|
<div class="submit-group panel">
|
||||||
|
@ -45,7 +83,7 @@
|
||||||
name="action" value="save"
|
name="action" value="save"
|
||||||
>
|
>
|
||||||
<i class="fa fa-check"></i>
|
<i class="fa fa-check"></i>
|
||||||
{% trans "Save" %}
|
{% translate "Save" %}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load compress %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>{{ request.event.name }} room info</title>
|
||||||
|
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
|
||||||
|
<script src="{% static "pretalx_broadcast_tools/room_info.js" %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />
|
||||||
|
{% if request.event and request.event.custom_css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ request.event.custom_css.url }}"/>
|
||||||
|
{% endif %}
|
||||||
|
</head>
|
||||||
|
<body id="broadcast_tools_room_info">
|
||||||
|
<div id="broadcast_tools_room_info_header">
|
||||||
|
<h1 id="broadcast_tools_room_info_roomname"></h1>
|
||||||
|
<h2 id="broadcast_tools_room_info_title">Loading ...</h2>
|
||||||
|
<h3 id="broadcast_tools_room_info_speaker">Content should appear soon. If not, please verify you have Javascript enabled.</h3>
|
||||||
|
</div>
|
||||||
|
<div id="broadcast_tools_room_info_qr"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load compress %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>{{ request.event.name }} room timer</title>
|
||||||
|
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
|
||||||
|
<script src="{% static "pretalx_broadcast_tools/room_timer.js" %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />
|
||||||
|
{% if request.event and request.event.custom_css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ request.event.custom_css.url }}"/>
|
||||||
|
{% endif %}
|
||||||
|
</head>
|
||||||
|
<body id="broadcast_tools_room_timer">
|
||||||
|
<div id="broadcast_tools_room_timer_header">
|
||||||
|
<h2 id="broadcast_tools_room_timer_title">Loading ...</h2>
|
||||||
|
<h3 id="broadcast_tools_room_timer_speaker">Content should appear soon. If not, please verify you have Javascript enabled.</h3>
|
||||||
|
<p id="broadcast_tools_room_timer_scheduledata"></p>
|
||||||
|
</div>
|
||||||
|
<div id="broadcast_tools_room_timer_timeleft">
|
||||||
|
<p id="broadcast_tools_room_timer_timeleft_timer"></p>
|
||||||
|
<p id="broadcast_tools_room_timer_timeleft_hint"></p>
|
||||||
|
</div>
|
||||||
|
<div id="broadcast_tools_room_timer_progressbar">
|
||||||
|
<p id="broadcast_tools_room_timer_progressbar_bar"> </p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,27 +1,67 @@
|
||||||
from django.urls import re_path
|
from django.urls import include, path
|
||||||
from pretalx.event.models.event import SLUG_CHARS
|
|
||||||
|
|
||||||
from . import views
|
from .views.event_info import BroadcastToolsEventInfoView
|
||||||
|
from .views.orga import BroadcastToolsOrgaView
|
||||||
|
from .views.qr import BroadcastToolsFeedbackQrCodeSvg, BroadcastToolsPublicQrCodeSvg
|
||||||
|
from .views.schedule import BroadcastToolsScheduleView
|
||||||
|
from .views.static_html import (
|
||||||
|
BroadcastToolsLowerThirdsView,
|
||||||
|
BroadcastToolsRoomInfoView,
|
||||||
|
BroadcastToolsRoomTimerView,
|
||||||
|
)
|
||||||
|
from .views.voctomix_export import BroadcastToolsLowerThirdsVoctomixDownloadView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(
|
path(
|
||||||
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/lower-thirds/$",
|
"<slug:event>/p/broadcast-tools/",
|
||||||
views.BroadcastToolsLowerThirdsView.as_view(),
|
include(
|
||||||
name="lowerthirds",
|
[
|
||||||
|
path(
|
||||||
|
"event.json",
|
||||||
|
BroadcastToolsEventInfoView.as_view(),
|
||||||
|
name="event_info",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"schedule.json",
|
||||||
|
BroadcastToolsScheduleView.as_view(),
|
||||||
|
name="schedule",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"lower-thirds/",
|
||||||
|
BroadcastToolsLowerThirdsView.as_view(),
|
||||||
|
name="lowerthirds",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"lower-thirds.voctomix.tar.gz",
|
||||||
|
BroadcastToolsLowerThirdsVoctomixDownloadView.as_view(),
|
||||||
|
name="lowerthirds_voctomix_download",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"feedback-qr/<talk>.svg",
|
||||||
|
BroadcastToolsFeedbackQrCodeSvg.as_view(),
|
||||||
|
name="feedback_qr_id",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public-qr/<talk>.svg",
|
||||||
|
BroadcastToolsPublicQrCodeSvg.as_view(),
|
||||||
|
name="public_qr_id",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"room-info/",
|
||||||
|
BroadcastToolsRoomInfoView.as_view(),
|
||||||
|
name="room_info",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"room-timer/",
|
||||||
|
BroadcastToolsRoomTimerView.as_view(),
|
||||||
|
name="room_timer",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
re_path(
|
path(
|
||||||
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/event.json$",
|
"orga/event/<slug:event>/settings/p/broadcast-tools/",
|
||||||
views.BroadcastToolsEventInfoView.as_view(),
|
BroadcastToolsOrgaView.as_view(),
|
||||||
name="event_info",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/schedule.json$",
|
|
||||||
views.BroadcastToolsScheduleView.as_view(),
|
|
||||||
name="schedule",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
f"^orga/event/(?P<event>[{SLUG_CHARS}]+)/settings/p/broadcast-tools/$",
|
|
||||||
views.BroadcastToolsOrgaView.as_view(),
|
|
||||||
name="orga",
|
name="orga",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
32
pretalx_broadcast_tools/utils/placeholders.py
Normal file
32
pretalx_broadcast_tools/utils/placeholders.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def placeholders(event, talk, supports_html_colour=False):
|
||||||
|
track_name = str(talk.submission.track.name) if talk.submission.track else ""
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"CODE": talk.submission.code,
|
||||||
|
"EVENT_SLUG": str(event.slug),
|
||||||
|
"FEEDBACK_URL": "{}{}".format(
|
||||||
|
event.custom_domain or settings.SITE_URL,
|
||||||
|
talk.submission.urls.feedback,
|
||||||
|
),
|
||||||
|
"TALK_SLUG": talk.frab_slug,
|
||||||
|
"TALK_URL": "{}{}".format(
|
||||||
|
event.custom_domain or settings.SITE_URL,
|
||||||
|
talk.submission.urls.public,
|
||||||
|
),
|
||||||
|
"TRACK_NAME": track_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if talk.submission.track and supports_html_colour:
|
||||||
|
result["TRACK_NAME_COLOURED"] = '<span style="color: {}">{}</span>'.format(
|
||||||
|
talk.submission.track.color, track_name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result["TRACK_NAME_COLOURED"] = track_name
|
||||||
|
|
||||||
|
# for the americans
|
||||||
|
result["TRACK_NAME_COLORED"] = result["TRACK_NAME_COLOURED"]
|
||||||
|
|
||||||
|
return result
|
|
@ -1,108 +0,0 @@
|
||||||
import datetime as dt
|
|
||||||
|
|
||||||
import pytz
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.views.generic import FormView
|
|
||||||
from django.views.generic.base import TemplateView
|
|
||||||
from pretalx.agenda.views.schedule import ScheduleMixin
|
|
||||||
from pretalx.common.mixins.views import EventPermissionRequired, PermissionRequired
|
|
||||||
from pretalx.schedule.exporters import ScheduleData
|
|
||||||
|
|
||||||
from .forms import BroadcastToolsSettingsForm
|
|
||||||
|
|
||||||
|
|
||||||
class BroadcastToolsLowerThirdsView(TemplateView):
|
|
||||||
template_name = "pretalx_broadcast_tools/lower_thirds.html"
|
|
||||||
|
|
||||||
|
|
||||||
class BroadcastToolsOrgaView(PermissionRequired, FormView):
|
|
||||||
form_class = BroadcastToolsSettingsForm
|
|
||||||
permission_required = "orga.change_settings"
|
|
||||||
template_name = "pretalx_broadcast_tools/orga.html"
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return self.request.path
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
form.save()
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
def get_object(self):
|
|
||||||
return self.request.event
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
return {
|
|
||||||
"obj": self.request.event,
|
|
||||||
"attribute_name": "settings",
|
|
||||||
"locales": self.request.event.locales,
|
|
||||||
**kwargs,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BroadcastToolsEventInfoView(TemplateView):
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
color = self.request.event.primary_color or "#3aa57c"
|
|
||||||
return JsonResponse(
|
|
||||||
{
|
|
||||||
"slug": self.request.event.slug,
|
|
||||||
"name": str(self.request.event.name),
|
|
||||||
"no_talk": str(self.request.event.settings.lower_thirds_no_talk_info),
|
|
||||||
"color": color,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BroadcastToolsScheduleView(EventPermissionRequired, ScheduleMixin, TemplateView):
|
|
||||||
permission_required = "agenda.view_schedule"
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
schedule = ScheduleData(
|
|
||||||
event=self.request.event,
|
|
||||||
schedule=self.schedule,
|
|
||||||
)
|
|
||||||
tz = pytz.timezone(schedule.event.timezone)
|
|
||||||
infoline = str(schedule.event.settings.infoline or "")
|
|
||||||
return JsonResponse(
|
|
||||||
{
|
|
||||||
"rooms": sorted(
|
|
||||||
{
|
|
||||||
str(room["name"])
|
|
||||||
for day in schedule.data
|
|
||||||
for room in day["rooms"]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"talks": [
|
|
||||||
{
|
|
||||||
"id": talk.submission.id,
|
|
||||||
"start": talk.start.astimezone(tz).isoformat(),
|
|
||||||
"end": (talk.start + dt.timedelta(minutes=talk.duration))
|
|
||||||
.astimezone(tz)
|
|
||||||
.isoformat(),
|
|
||||||
"slug": talk.frab_slug,
|
|
||||||
"title": talk.submission.title,
|
|
||||||
"persons": sorted(
|
|
||||||
{
|
|
||||||
person.get_display_name()
|
|
||||||
for person in talk.submission.speakers.all()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"track": {
|
|
||||||
"color": talk.submission.track.color,
|
|
||||||
"name": str(talk.submission.track.name),
|
|
||||||
}
|
|
||||||
if talk.submission.track
|
|
||||||
else None,
|
|
||||||
"room": str(room["name"]),
|
|
||||||
"infoline": infoline.format(
|
|
||||||
EVENT_SLUG=str(schedule.event.slug),
|
|
||||||
TALK_SLUG=talk.frab_slug,
|
|
||||||
CODE=talk.submission.code,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for day in schedule.data
|
|
||||||
for room in day["rooms"]
|
|
||||||
for talk in room["talks"]
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
34
pretalx_broadcast_tools/views/event_info.py
Normal file
34
pretalx_broadcast_tools/views/event_info.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsEventInfoView(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
color = self.request.event.primary_color or "#3aa57c"
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"color": color,
|
||||||
|
"name": self.request.event.name.localize(self.request.event.locale),
|
||||||
|
"no_talk": str(
|
||||||
|
self.request.event.settings.broadcast_tools_lower_thirds_no_talk_info
|
||||||
|
),
|
||||||
|
"room-info": {
|
||||||
|
"lower_info": self.request.event.settings.broadcast_tools_room_info_lower_content
|
||||||
|
or "",
|
||||||
|
"show_next_talk": (
|
||||||
|
True
|
||||||
|
if self.request.event.settings.broadcast_tools_room_info_show_next_talk
|
||||||
|
else False
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"rooms": {
|
||||||
|
str(room.uuid): room.name.localize(self.request.event.locale)
|
||||||
|
for room in self.request.event.rooms.all()
|
||||||
|
},
|
||||||
|
"slug": self.request.event.slug,
|
||||||
|
"start": self.request.event.date_from.isoformat(),
|
||||||
|
"end": self.request.event.date_to.isoformat(),
|
||||||
|
"timezone": str(self.request.event.tz),
|
||||||
|
"locale": self.request.event.locale,
|
||||||
|
},
|
||||||
|
)
|
29
pretalx_broadcast_tools/views/orga.py
Normal file
29
pretalx_broadcast_tools/views/orga.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from django.views.generic import FormView
|
||||||
|
from pretalx.common.views.mixins import PermissionRequired
|
||||||
|
|
||||||
|
from ..forms import BroadcastToolsSettingsForm
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsOrgaView(PermissionRequired, FormView):
|
||||||
|
form_class = BroadcastToolsSettingsForm
|
||||||
|
permission_required = "orga.change_settings"
|
||||||
|
template_name = "pretalx_broadcast_tools/orga.html"
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.path
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.request.event
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
return {
|
||||||
|
"obj": self.request.event,
|
||||||
|
"attribute_name": "settings",
|
||||||
|
"locales": self.request.event.locales,
|
||||||
|
**kwargs,
|
||||||
|
}
|
30
pretalx_broadcast_tools/views/qr.py
Normal file
30
pretalx_broadcast_tools/views/qr.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
import qrcode
|
||||||
|
import qrcode.image.svg
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsFeedbackQrCodeSvg(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
talk = self.request.event.submissions.filter(id=kwargs["talk"]).first()
|
||||||
|
domain = request.event.custom_domain or settings.SITE_URL
|
||||||
|
image = qrcode.make(
|
||||||
|
f"{domain}{talk.urls.feedback}", image_factory=qrcode.image.svg.SvgImage
|
||||||
|
)
|
||||||
|
svg_data = mark_safe(ElementTree.tostring(image.get_image()).decode())
|
||||||
|
return HttpResponse(svg_data, content_type="image/svg+xml")
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsPublicQrCodeSvg(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
talk = self.request.event.submissions.filter(id=kwargs["talk"]).first()
|
||||||
|
domain = request.event.custom_domain or settings.SITE_URL
|
||||||
|
image = qrcode.make(
|
||||||
|
f"{domain}{talk.urls.public}", image_factory=qrcode.image.svg.SvgImage
|
||||||
|
)
|
||||||
|
svg_data = mark_safe(ElementTree.tostring(image.get_image()).decode())
|
||||||
|
return HttpResponse(svg_data, content_type="image/svg+xml")
|
122
pretalx_broadcast_tools/views/schedule.py
Normal file
122
pretalx_broadcast_tools/views/schedule.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.views import View
|
||||||
|
from pretalx.agenda.views.schedule import ScheduleMixin
|
||||||
|
from pretalx.common.views.mixins import EventPermissionRequired
|
||||||
|
from pretalx.schedule.exporters import ScheduleData
|
||||||
|
|
||||||
|
from ..utils.placeholders import placeholders
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsScheduleView(EventPermissionRequired, ScheduleMixin, View):
|
||||||
|
permission_required = "agenda.view_schedule"
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
schedule = ScheduleData(
|
||||||
|
event=self.request.event,
|
||||||
|
schedule=self.schedule,
|
||||||
|
)
|
||||||
|
infoline = str(
|
||||||
|
schedule.event.settings.broadcast_tools_lower_thirds_info_string or ""
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"rooms": sorted(
|
||||||
|
{
|
||||||
|
room["name"].localize(schedule.event.locale)
|
||||||
|
for day in schedule.data
|
||||||
|
for room in day["rooms"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"talks": [
|
||||||
|
{
|
||||||
|
"id": talk.submission.id,
|
||||||
|
"start": talk.start.astimezone(
|
||||||
|
schedule.event.tz
|
||||||
|
).isoformat(),
|
||||||
|
"start_ts": int(talk.start.timestamp()),
|
||||||
|
"end": (talk.start + dt.timedelta(minutes=talk.duration))
|
||||||
|
.astimezone(schedule.event.tz)
|
||||||
|
.isoformat(),
|
||||||
|
"end_ts": int(
|
||||||
|
(
|
||||||
|
talk.start + dt.timedelta(minutes=talk.duration)
|
||||||
|
).timestamp()
|
||||||
|
),
|
||||||
|
"slug": talk.frab_slug,
|
||||||
|
"title": talk.submission.title,
|
||||||
|
"persons": [
|
||||||
|
person.get_display_name()
|
||||||
|
for person in talk.submission.speakers.all()
|
||||||
|
],
|
||||||
|
"track": (
|
||||||
|
{
|
||||||
|
"color": talk.submission.track.color,
|
||||||
|
"name": str(talk.submission.track.name),
|
||||||
|
}
|
||||||
|
if talk.submission.track
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"room": room["name"].localize(schedule.event.locale),
|
||||||
|
"infoline": infoline.format(
|
||||||
|
**placeholders(
|
||||||
|
schedule.event, talk, supports_html_colour=True
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"image_url": talk.submission.image_url,
|
||||||
|
"locale": talk.submission.content_locale,
|
||||||
|
"do_not_record": talk.submission.do_not_record,
|
||||||
|
"abstract": talk.submission.abstract,
|
||||||
|
"urls": {
|
||||||
|
"feedback": "{}{}".format(
|
||||||
|
schedule.event.custom_domain or settings.SITE_URL,
|
||||||
|
talk.submission.urls.feedback,
|
||||||
|
),
|
||||||
|
"feedback_qr": reverse(
|
||||||
|
"plugins:pretalx_broadcast_tools:feedback_qr_id",
|
||||||
|
kwargs={
|
||||||
|
"event": schedule.event.slug,
|
||||||
|
"talk": talk.submission.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"public": "{}{}".format(
|
||||||
|
schedule.event.custom_domain or settings.SITE_URL,
|
||||||
|
talk.submission.urls.public,
|
||||||
|
),
|
||||||
|
"public_qr": reverse(
|
||||||
|
"plugins:pretalx_broadcast_tools:public_qr_id",
|
||||||
|
kwargs={
|
||||||
|
"event": schedule.event.slug,
|
||||||
|
"talk": talk.submission.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for day in schedule.data
|
||||||
|
for room in day["rooms"]
|
||||||
|
for talk in room["talks"]
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
key = str(e)[1:-1]
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"error": [
|
||||||
|
f"Could not find value for placeholder {{{key}}} in info line.",
|
||||||
|
f"If you want to use {{{key}}} without evaluating it, please use as follows: {{{{{key}}}}}",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"error": [
|
||||||
|
repr(e),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
13
pretalx_broadcast_tools/views/static_html.py
Normal file
13
pretalx_broadcast_tools/views/static_html.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsLowerThirdsView(TemplateView):
|
||||||
|
template_name = "pretalx_broadcast_tools/lower_thirds.html"
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsRoomInfoView(TemplateView):
|
||||||
|
template_name = "pretalx_broadcast_tools/room_info.html"
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsRoomTimerView(TemplateView):
|
||||||
|
template_name = "pretalx_broadcast_tools/room_timer.html"
|
22
pretalx_broadcast_tools/views/voctomix_export.py
Normal file
22
pretalx_broadcast_tools/views/voctomix_export.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from django.http import FileResponse, Http404
|
||||||
|
from django.views import View
|
||||||
|
from pretalx.common.text.path import safe_filename
|
||||||
|
from pretalx.common.views.mixins import EventPermissionRequired
|
||||||
|
|
||||||
|
from pretalx_broadcast_tools.management.commands.export_voctomix_lower_thirds import (
|
||||||
|
get_export_targz_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastToolsLowerThirdsVoctomixDownloadView(EventPermissionRequired, View):
|
||||||
|
permission_required = "agenda.view_schedule"
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
targz_path = get_export_targz_path(self.request.event)
|
||||||
|
if not targz_path.exists():
|
||||||
|
raise Http404()
|
||||||
|
response = FileResponse(open(targz_path, "rb"), as_attachment=True)
|
||||||
|
response["Content-Disposition"] = (
|
||||||
|
f"attachment; filename={safe_filename(targz_path.name)}"
|
||||||
|
)
|
||||||
|
return response
|
41
pyproject.toml
Normal file
41
pyproject.toml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
[project]
|
||||||
|
name = "pretalx-broadcast-tools"
|
||||||
|
dynamic = ["version"]
|
||||||
|
description = """
|
||||||
|
Some tools which can be used for supporting a broadcasting software.
|
||||||
|
This currently includes a generator for PDF printouts, a 'lower thirds'
|
||||||
|
endpoint, and a full-screen webpage showing information about the
|
||||||
|
currently running talk.
|
||||||
|
"""
|
||||||
|
readme = "README.rst"
|
||||||
|
license = {text = "Apache Software License"}
|
||||||
|
keywords = ["pretalx"]
|
||||||
|
authors = [
|
||||||
|
{name = "Franziska Kunsmann", email = "git@kunsmann.eu"},
|
||||||
|
]
|
||||||
|
maintainers = [
|
||||||
|
{name = "Franziska Kunsmann", email = "git@kunsmann.eu"},
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.entry-points."pretalx.plugin"]
|
||||||
|
pretalx_broadcast_tools = "pretalx_broadcast_tools:PretalxPluginMeta"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
homepage = "https://github.com/Kunsi/pretalx-plugin-broadcast-tools"
|
||||||
|
repository = "https://github.com/Kunsi/pretalx-plugin-broadcast-tools.git"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
include-package-data = true
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "pretalx_broadcast_tools.__version__"}
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["pretalx*"]
|
46
setup.py
46
setup.py
|
@ -1,46 +0,0 @@
|
||||||
import os
|
|
||||||
from distutils.command.build import build
|
|
||||||
|
|
||||||
from django.core import management
|
|
||||||
from setuptools import find_packages, setup
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(
|
|
||||||
os.path.join(os.path.dirname(__file__), "README.rst"), encoding="utf-8"
|
|
||||||
) as f:
|
|
||||||
long_description = f.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
long_description = ""
|
|
||||||
|
|
||||||
|
|
||||||
class CustomBuild(build):
|
|
||||||
def run(self):
|
|
||||||
management.call_command("compilemessages", verbosity=1)
|
|
||||||
build.run(self)
|
|
||||||
|
|
||||||
|
|
||||||
cmdclass = {"build": CustomBuild}
|
|
||||||
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="pretalx-plugin-broadcast-tools",
|
|
||||||
version="0.2,0",
|
|
||||||
description=(
|
|
||||||
"Some tools which can be used for supporting a broadcasting "
|
|
||||||
"software, for example a 'lower third' page which can be "
|
|
||||||
"embedded into your broadcasting software"
|
|
||||||
),
|
|
||||||
long_description=long_description,
|
|
||||||
url="https://github.com/Kunsi/pretalx-plugin-broadcast-tools",
|
|
||||||
author="kunsi",
|
|
||||||
author_email="git@kunsmann.eu",
|
|
||||||
license="Apache Software License",
|
|
||||||
install_requires=[],
|
|
||||||
packages=find_packages(exclude=["tests", "tests.*"]),
|
|
||||||
include_package_data=True,
|
|
||||||
cmdclass=cmdclass,
|
|
||||||
entry_points="""
|
|
||||||
[pretalx.plugin]
|
|
||||||
pretalx_broadcast_tools=pretalx_broadcast_tools:PretalxPluginMeta
|
|
||||||
""",
|
|
||||||
)
|
|
Loading…
Add table
Reference in a new issue