mirror of
https://github.com/Kunsi/scheduled-plugin-pretalx-broadcast-tools.git
synced 2024-11-21 09:31:06 +00:00
first draft, somewhat working
time transfer does not work
This commit is contained in:
commit
9e2c26e5d9
13 changed files with 2258 additions and 0 deletions
235
anims.lua
Normal file
235
anims.lua
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local function make_smooth(timeline)
|
||||||
|
assert(#timeline >= 1)
|
||||||
|
|
||||||
|
local function find_span(t)
|
||||||
|
local lo, hi = 1, #timeline
|
||||||
|
while lo <= hi do
|
||||||
|
local mid = math.floor((lo+hi)/2)
|
||||||
|
if timeline[mid].t > t then
|
||||||
|
hi = mid - 1
|
||||||
|
else
|
||||||
|
lo = mid + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return math.max(1, lo-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_value(t)
|
||||||
|
local t1 = find_span(t)
|
||||||
|
local t0 = math.max(1, t1-1)
|
||||||
|
local t2 = math.min(#timeline, t1+1)
|
||||||
|
local t3 = math.min(#timeline, t1+2)
|
||||||
|
|
||||||
|
local p0 = timeline[t0]
|
||||||
|
local p1 = timeline[t1]
|
||||||
|
local p2 = timeline[t2]
|
||||||
|
local p3 = timeline[t3]
|
||||||
|
|
||||||
|
local v0 = p0.val
|
||||||
|
local v1 = p1.val
|
||||||
|
local v2 = p2.val
|
||||||
|
local v3 = p3.val
|
||||||
|
|
||||||
|
local progress = 0.0
|
||||||
|
if p1.t ~= p2.t then
|
||||||
|
progress = math.min(1, math.max(0, 1.0 / (p2.t - p1.t) * (t - p1.t)))
|
||||||
|
end
|
||||||
|
|
||||||
|
if p1.ease == "linear" then
|
||||||
|
return (v1 * (1-progress) + (v2 * progress))
|
||||||
|
elseif p1.ease == "step" then
|
||||||
|
return v1
|
||||||
|
elseif p1.ease == "inout" then
|
||||||
|
return -(v2-v1) * progress*(progress-2) + v1
|
||||||
|
else
|
||||||
|
local d1 = p2.t - p1.t
|
||||||
|
local d0 = p1.t - p0.t
|
||||||
|
|
||||||
|
local bias = 0.5
|
||||||
|
local tension = 0.8
|
||||||
|
local mu = progress
|
||||||
|
local mu2 = mu * mu
|
||||||
|
local mu3 = mu2 * mu
|
||||||
|
local m0 = (v1-v0)*(1+bias)*(1-tension)/2 + (v2-v1)*(1-bias)*(1-tension)/2
|
||||||
|
local m1 = (v2-v1)*(1+bias)*(1-tension)/2 + (v3-v2)*(1-bias)*(1-tension)/2
|
||||||
|
|
||||||
|
m0 = m0 * (2*d1)/(d0+d1)
|
||||||
|
m1 = m1 * (2*d0)/(d0+d1)
|
||||||
|
local a0 = 2*mu3 - 3*mu2 + 1
|
||||||
|
local a1 = mu3 - 2*mu2 + mu
|
||||||
|
local a2 = mu3 - mu2
|
||||||
|
local a3 = -2*mu3 + 3*mu2
|
||||||
|
return a0*v1+a1*m0+a2*m1+a3*v2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return get_value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rotating_entry_exit(S, E, obj)
|
||||||
|
local rotate = make_smooth{
|
||||||
|
{t = S , val = -5},
|
||||||
|
{t = S+.3,val = 0, ease='step'},
|
||||||
|
{t = E-.3,val = 0},
|
||||||
|
{t = E, val = 5},
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(t)
|
||||||
|
gl.rotate(rotate(t), 0, 1, 0)
|
||||||
|
return obj(t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function move_in_move_out(S, E, x, y, obj)
|
||||||
|
local x = make_smooth{
|
||||||
|
{t = S, val = x+50},
|
||||||
|
{t = S+.3,val = x, ease='step'},
|
||||||
|
{t = E-.3,val = x},
|
||||||
|
{t = E, val = x-20},
|
||||||
|
}
|
||||||
|
|
||||||
|
local y = make_smooth{
|
||||||
|
{t = S, val = y*1.05},
|
||||||
|
{t = S+.3,val = y, ease='step'},
|
||||||
|
{t = E-.3,val = y},
|
||||||
|
{t = E, val = y*0.95},
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(t)
|
||||||
|
gl.translate(x(t), y(t))
|
||||||
|
return obj(t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local M = {
|
||||||
|
make_smooth = make_smooth;
|
||||||
|
rotating_entry_exit = rotating_entry_exit;
|
||||||
|
move_in_move_out = move_in_move_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function M.Area(width, height)
|
||||||
|
local anims = {}
|
||||||
|
|
||||||
|
local function add(anim, pos)
|
||||||
|
pos = pos or #anims+1
|
||||||
|
table.insert(anims, pos, anim)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw(t, x1, y1, x2, y2)
|
||||||
|
local w = x2 - x1
|
||||||
|
local h = y2 - y1
|
||||||
|
local ax1, ay1, ax2, ay2 = util.scale_into(w, h, width, height)
|
||||||
|
local s = (ax2 - ax1) / width
|
||||||
|
gl.pushMatrix()
|
||||||
|
gl.translate(x1+ax1, y1+ay1)
|
||||||
|
gl.scale(s, s)
|
||||||
|
for idx = 1, #anims do
|
||||||
|
gl.pushMatrix()
|
||||||
|
anims[idx](t)
|
||||||
|
gl.popMatrix()
|
||||||
|
end
|
||||||
|
gl.popMatrix()
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
add = add;
|
||||||
|
draw = draw;
|
||||||
|
width = width;
|
||||||
|
height = height;
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.moving_font(S, E, font, x, y, text, size, r, g, b, a)
|
||||||
|
local alpha = make_smooth{
|
||||||
|
{t = S, val = 0},
|
||||||
|
{t = S+.5,val = 1, ease='step'},
|
||||||
|
{t = E-.5,val = 1},
|
||||||
|
{t = E, val = 0},
|
||||||
|
}
|
||||||
|
return move_in_move_out(S, E, x, y,
|
||||||
|
rotating_entry_exit(S, E, function(t)
|
||||||
|
return font:write(0, 0, text, size, r, g, b, a * alpha(t))
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.moving_image_raw(S, E, img, x1, y1, x2, y2, a)
|
||||||
|
a = a or 1
|
||||||
|
local alpha = make_smooth{
|
||||||
|
{t = S, val = 0},
|
||||||
|
{t = S+.5,val = 1, ease='step'},
|
||||||
|
{t = E-.5,val = 1},
|
||||||
|
{t = E, val = 0},
|
||||||
|
}
|
||||||
|
return move_in_move_out(S, E, x1, y1,
|
||||||
|
rotating_entry_exit(S, E, function(t)
|
||||||
|
return img:draw(0, 0, x2-x1, y2-y1, a * alpha(t))
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.moving_image(S, E, img, x1, y1, x2, y2, a)
|
||||||
|
local alpha = make_smooth{
|
||||||
|
{t = S, val = 0},
|
||||||
|
{t = S+.5,val = 1, ease='step'},
|
||||||
|
{t = E-.5,val = 1},
|
||||||
|
{t = E, val = 0},
|
||||||
|
}
|
||||||
|
return move_in_move_out(S, E, x1, y1,
|
||||||
|
rotating_entry_exit(S, E, function(t)
|
||||||
|
return util.draw_correct(img, 0, 0, x2-x1, y2-y1, a * alpha(t))
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.logo(S, E, x, y, img, size)
|
||||||
|
local alpha = make_smooth{
|
||||||
|
{t = S+0, val = 0},
|
||||||
|
{t = S+.5, val = 1, ease='step'},
|
||||||
|
{t = E-.5, val = 1},
|
||||||
|
{t = E, val = 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(t)
|
||||||
|
return util.draw_correct(img, x, y, x+size, y+size, alpha(t))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.tweet_profile(S, E, x, y, img, size)
|
||||||
|
local x = make_smooth{
|
||||||
|
{t = S+0, val = 1800},
|
||||||
|
{t = S+1, val = 500},
|
||||||
|
{t = S+2, val = x, ease='step'},
|
||||||
|
{t = E-1, val = x},
|
||||||
|
{t = E, val = -20},
|
||||||
|
}
|
||||||
|
local y = make_smooth{
|
||||||
|
{t = S+0, val = 500},
|
||||||
|
{t = S+1, val = 200},
|
||||||
|
{t = S+2, val = y, ease='step'},
|
||||||
|
{t = E-1, val = y},
|
||||||
|
{t = E, val = 0},
|
||||||
|
}
|
||||||
|
local scale = make_smooth{
|
||||||
|
{t = S , val = 0},
|
||||||
|
{t = S+1, val = 8},
|
||||||
|
{t = S+2, val = 1, ease='step'},
|
||||||
|
{t = E-1, val = 1},
|
||||||
|
{t = E, val = 0},
|
||||||
|
}
|
||||||
|
local alpha = make_smooth{
|
||||||
|
{t = S, val = 0},
|
||||||
|
{t = S+1, val = 1, ease='step'},
|
||||||
|
{t = E-1, val = 1},
|
||||||
|
{t = E, val = 0},
|
||||||
|
}
|
||||||
|
return function(t)
|
||||||
|
local size = scale(t) * size
|
||||||
|
gl.translate(x(t), y(t))
|
||||||
|
return util.draw_correct(img, 0, 0, size, size, alpha(t))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
BIN
empty.png
Normal file
BIN
empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 105 B |
BIN
font.ttf
Normal file
BIN
font.ttf
Normal file
Binary file not shown.
58
node.json
Normal file
58
node.json
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"name": "Plugin: pretalx-broadcast-tools",
|
||||||
|
"permissions": {
|
||||||
|
"network": "Has to fetch schedule from remote site"
|
||||||
|
},
|
||||||
|
"options": [{
|
||||||
|
"title": "Design",
|
||||||
|
"type": "section"
|
||||||
|
}, {
|
||||||
|
"title": "Font",
|
||||||
|
"ui_width": 6,
|
||||||
|
"name": "font",
|
||||||
|
"type": "font",
|
||||||
|
"default": "silkscreen.ttf"
|
||||||
|
}, {
|
||||||
|
"title": "Show Language",
|
||||||
|
"ui_width": 4,
|
||||||
|
"name": "show_language",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
}, {
|
||||||
|
"title": "Show Track",
|
||||||
|
"ui_width": 4,
|
||||||
|
"name": "show_track",
|
||||||
|
"type": "boolean",
|
||||||
|
"hint": "Show colored bars in 'all talks', track name in 'next talk'",
|
||||||
|
"default": true
|
||||||
|
}, {
|
||||||
|
"title": "Schedule",
|
||||||
|
"type": "section"
|
||||||
|
}, {
|
||||||
|
"title": "Event URL",
|
||||||
|
"name": "event_url",
|
||||||
|
"type": "string",
|
||||||
|
"default": "https://pretalx.example.com/my-super-cool-event/"
|
||||||
|
}, {
|
||||||
|
"title": "Rooms",
|
||||||
|
"name": "rooms",
|
||||||
|
"doc_link": true,
|
||||||
|
"type": "list",
|
||||||
|
"itemname": "Room",
|
||||||
|
"hint": "Define all rooms in your schedule and assign them to devices",
|
||||||
|
"items": [{
|
||||||
|
"title": "Room Name",
|
||||||
|
"ui_width": 3,
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "Name of this room in your events native language",
|
||||||
|
"default": ""
|
||||||
|
}, {
|
||||||
|
"title": "Serial",
|
||||||
|
"ui_width": 3,
|
||||||
|
"name": "serial",
|
||||||
|
"type": "device",
|
||||||
|
"default": ""
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
7
node.lua
Normal file
7
node.lua
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
util.init_hosted()
|
||||||
|
|
||||||
|
gl.setup(NATIVE_WIDTH, NATIVE_HEIGHT)
|
||||||
|
|
||||||
|
function node.render()
|
||||||
|
CONFIG.font:write(10, 10, "This is a plugin for scheduled player, don't use it individually", 30, 1,1,1,1)
|
||||||
|
end
|
BIN
node.png
Normal file
BIN
node.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
14
package.json
Normal file
14
package.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "pretalx-broadcast-tools",
|
||||||
|
"author": "git@kunsmann.eu",
|
||||||
|
"desc": "Shows information provided by pretalx-broadcast-tools",
|
||||||
|
"nesting": {
|
||||||
|
"parents": ["scheduled-plugin"],
|
||||||
|
"childs": []
|
||||||
|
},
|
||||||
|
"platforms": ["pi/epoch-1", "pi/epoch-2"],
|
||||||
|
"offline": {
|
||||||
|
"support": "no",
|
||||||
|
"info": "Needs to fetch remote content"
|
||||||
|
}
|
||||||
|
}
|
BIN
package.png
Normal file
BIN
package.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
118
service
Executable file
118
service
Executable file
|
@ -0,0 +1,118 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from requests import get
|
||||||
|
|
||||||
|
from hosted import config, node
|
||||||
|
|
||||||
|
config.restart_on_update()
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
sys.stderr.write("[pretalx] {}\n".format(msg))
|
||||||
|
|
||||||
|
|
||||||
|
def idle(seconds, event_start, event_tz):
|
||||||
|
end = time.time() + seconds
|
||||||
|
log("sleeping for {} seconds".format(seconds))
|
||||||
|
while time.time() < end:
|
||||||
|
send_data = {
|
||||||
|
'day': '??',
|
||||||
|
}
|
||||||
|
if event_start is not None:
|
||||||
|
timezone = pytz.timezone(event_tz)
|
||||||
|
event_now = datetime.now(timezone)
|
||||||
|
utc_now = datetime.now(pytz.utc)
|
||||||
|
|
||||||
|
utc_offset = (event_now - utc_now).total_seconds()
|
||||||
|
|
||||||
|
day_zero = event_start.replace(
|
||||||
|
hour=0,
|
||||||
|
minute=0,
|
||||||
|
second=0,
|
||||||
|
tzinfo=timezone
|
||||||
|
) - timedelta(days=1)
|
||||||
|
|
||||||
|
day_info = event_now - day_zero
|
||||||
|
|
||||||
|
log("Day0: {}".format(day_zero.isoformat()))
|
||||||
|
log("NOW: {}".format(event_now.isoformat()))
|
||||||
|
|
||||||
|
send_data['day'] = day_info.days
|
||||||
|
send_data['time'] = int(time.time() + utc_offset)
|
||||||
|
else:
|
||||||
|
send_data['time'] = time.time()
|
||||||
|
|
||||||
|
for k,v in send_data.items():
|
||||||
|
node.send_raw('root/plugin/pretalx/{}:{}'.format(k, v))
|
||||||
|
node.send_raw('root/plugin/pretalx-broadcast-tools/{}:{}'.format(k, v))
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
event_info = None
|
||||||
|
event_start = None
|
||||||
|
schedule = None
|
||||||
|
event_tz = 'UTC'
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event_url = config["event_url"]
|
||||||
|
if not event_url.endswith("/"):
|
||||||
|
event_url = event_url + "/"
|
||||||
|
if "example.com" in event_url:
|
||||||
|
log("default event url, waiting for config update")
|
||||||
|
# sleep forever, service will get restarted if the config
|
||||||
|
# changes.
|
||||||
|
time.sleep(99999999)
|
||||||
|
|
||||||
|
log("event url: {}".format(event_url))
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = get(
|
||||||
|
event_url + "p/broadcast-tools/event.json",
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
log("updating event info failed: {}".format(repr(e)))
|
||||||
|
# Only print the error message. If we have fetched the event
|
||||||
|
# info json blob atleast once, we have all the information
|
||||||
|
# we need.
|
||||||
|
else:
|
||||||
|
event_info = r.json()
|
||||||
|
node.write_json("event.json", event_info)
|
||||||
|
log("updated event info json")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = get(
|
||||||
|
event_url + "p/broadcast-tools/schedule.json",
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
log("updating schedule failed: {}".format(repr(e)))
|
||||||
|
# Only print the error message. If we have fetched the schedule
|
||||||
|
# info json blob atleast once, we have all the information
|
||||||
|
# we need.
|
||||||
|
else:
|
||||||
|
schedule = r.json()
|
||||||
|
for talk in schedule['talks']:
|
||||||
|
talk['start_str'] = datetime.fromtimestamp(talk['start_ts']).strftime('%H:%M')
|
||||||
|
node.write_json("schedule.json", schedule)
|
||||||
|
log("updated schedule json")
|
||||||
|
|
||||||
|
if event_info is not None:
|
||||||
|
event_start = datetime.strptime(event_info["start"], "%Y-%m-%d")
|
||||||
|
event_tz = event_info['timezone']
|
||||||
|
|
||||||
|
idle(30, event_start, event_tz)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
time.sleep(30)
|
BIN
silkscreen.ttf
Normal file
BIN
silkscreen.ttf
Normal file
Binary file not shown.
94
tile.js
Normal file
94
tile.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
var config = {
|
||||||
|
props: ['config'],
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<h4>Frab Plugin</h4>
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-xs-3'>
|
||||||
|
<select class='btn btn-default' v-model="mode">
|
||||||
|
<option value="all_talks">All Talks</option>
|
||||||
|
<option value="next_talk">Next Talk</option>
|
||||||
|
<option value="room">Room Name</option>
|
||||||
|
<option value="day">Day</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class='col-xs-3'>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
v-model="color"
|
||||||
|
class='form-control'/>
|
||||||
|
</div>
|
||||||
|
<div class='col-xs-3'>
|
||||||
|
<select class='btn btn-default' v-model="font_size">
|
||||||
|
<option value="40">40px</option>
|
||||||
|
<option value="50">50px</option>
|
||||||
|
<option value="60">60px</option>
|
||||||
|
<option value="70">70px</option>
|
||||||
|
<option value="80">80px</option>
|
||||||
|
<option value="90">90px</option>
|
||||||
|
<option value="100">100px</option>
|
||||||
|
<option value="110">110px</option>
|
||||||
|
<option value="150">150px</option>
|
||||||
|
<option value="200">200px</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if='mode == "all_talks"'>
|
||||||
|
<h4>All Talks options</h4>
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-xs-3'>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="all_speakers"
|
||||||
|
class='form-check-input'/>
|
||||||
|
Show speaker names
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if='mode == "next_talk"'>
|
||||||
|
<h4>Next Talk options</h4>
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-xs-3'>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="next_abstract"
|
||||||
|
class='form-check-input'/>
|
||||||
|
Show abstract
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if='mode == "day"'>
|
||||||
|
<h4>Clock options</h4>
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-xs-3'>
|
||||||
|
<select class='btn btn-default' v-model="day_align">
|
||||||
|
<option value="left">Align left</option>
|
||||||
|
<option value="center">Align centered</option>
|
||||||
|
<option value="right">Align right</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class='col-xs-3'>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="day_template"
|
||||||
|
placeholder="Template: 'Day %s'"
|
||||||
|
class='form-control'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
computed: {
|
||||||
|
mode: ChildTile.config_value('mode', 'all_talks'),
|
||||||
|
color: ChildTile.config_value('color', '#ffffff'),
|
||||||
|
font_size: ChildTile.config_value('font_size', 70, parseInt),
|
||||||
|
all_names: ChildTile.config_value('all_names', true),
|
||||||
|
next_abstract: ChildTile.config_value('next_abstract', false),
|
||||||
|
day_align: ChildTile.config_value('day_align', 'left'),
|
||||||
|
day_template: ChildTile.config_value('day_template', 'Day %d'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChildTile.register({
|
||||||
|
config: config,
|
||||||
|
});
|
358
tile.lua
Normal file
358
tile.lua
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
local api, CHILDS, CONTENTS = ...
|
||||||
|
|
||||||
|
local json = require "json"
|
||||||
|
local helper = require "helper"
|
||||||
|
local anims = require(api.localized "anims")
|
||||||
|
|
||||||
|
local font
|
||||||
|
local white = resource.create_colored_texture(1,1,1)
|
||||||
|
local fallback_track_background = resource.create_colored_texture(.5,.5,.5,1)
|
||||||
|
|
||||||
|
local schedule = {}
|
||||||
|
local event = {}
|
||||||
|
local rooms = {}
|
||||||
|
local all_next_talks = {}
|
||||||
|
local room_next_talks = {}
|
||||||
|
local current_room
|
||||||
|
local day = 0
|
||||||
|
local time = 1695989869
|
||||||
|
local show_language = true
|
||||||
|
local show_track = true
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local function rgba(base, a)
|
||||||
|
return base[1], base[2], base[3], a
|
||||||
|
end
|
||||||
|
|
||||||
|
local function log(what)
|
||||||
|
return print("[pretalx] " .. what)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.data_trigger(path, data)
|
||||||
|
log("received data '" .. data .. "' on " .. path)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.updated_config_json(config)
|
||||||
|
log("running on device ".. tostring(sys.get_env "SERIAL"))
|
||||||
|
font = resource.load_font(api.localized(config.font.asset_name))
|
||||||
|
show_language = config.show_language
|
||||||
|
show_track = config.show_track
|
||||||
|
|
||||||
|
current_room = nil
|
||||||
|
for idx, room in ipairs(config.rooms) do
|
||||||
|
log(tostring(room.serial) .. " room '" .. room.name .. "'")
|
||||||
|
if room.serial == sys.get_env "SERIAL" then
|
||||||
|
log("found my room: ", room.name)
|
||||||
|
current_room = room.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.updated_schedule_json(new_schedule)
|
||||||
|
log("new schedule")
|
||||||
|
schedule = new_schedule.talks
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.updated_event_json(new_info)
|
||||||
|
log("new event info")
|
||||||
|
event = new_info
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wrap(str, font, size, max_w)
|
||||||
|
local lines = {}
|
||||||
|
local space_w = font:width(" ", size)
|
||||||
|
|
||||||
|
local remaining = max_w
|
||||||
|
local line = {}
|
||||||
|
for non_space in str:gmatch("%S+") do
|
||||||
|
local w = font:width(non_space, size)
|
||||||
|
if remaining - w < 0 then
|
||||||
|
lines[#lines+1] = table.concat(line, "")
|
||||||
|
line = {}
|
||||||
|
remaining = max_w
|
||||||
|
end
|
||||||
|
line[#line+1] = non_space
|
||||||
|
line[#line+1] = " "
|
||||||
|
remaining = remaining - w - space_w
|
||||||
|
end
|
||||||
|
if #line > 0 then
|
||||||
|
lines[#lines+1] = table.concat(line, "")
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check_next_talks()
|
||||||
|
log("time is now " .. time)
|
||||||
|
if time == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
room_next_talks = {}
|
||||||
|
all_next_talks = {}
|
||||||
|
|
||||||
|
local min_start = time - 25 * 60
|
||||||
|
|
||||||
|
log("my room is '" .. current_room .. "'")
|
||||||
|
|
||||||
|
for idx = 1, #schedule do
|
||||||
|
local talk = schedule[idx]
|
||||||
|
|
||||||
|
-- Ignore all talks that started long before now or have already
|
||||||
|
-- ended here. We don't want to announce these.
|
||||||
|
if talk.start_ts > min_start and talk.end_ts > time then
|
||||||
|
-- is this in *this* room, or somewhere else?
|
||||||
|
if current_room and talk.room == current_room then
|
||||||
|
room_next_talks[#room_next_talks+1] = talk
|
||||||
|
end
|
||||||
|
all_next_talks[#all_next_talks+1] = talk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sort_talks(a, b)
|
||||||
|
return a.start_ts < b.start_ts or (a.start_ts == b.start_ts and a.room < b.room)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(room_next_talks, sort_talks)
|
||||||
|
table.sort(all_next_talks, sort_talks)
|
||||||
|
|
||||||
|
log(tostring(#all_next_talks) .. " talks to come")
|
||||||
|
log(tostring(#room_next_talks) .. " in this room")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function view_next_talk(starts, ends, config, x1, y1, x2, y2)
|
||||||
|
local font_size = config.font_size or 70
|
||||||
|
local show_abstract = config.next_abstract
|
||||||
|
local default_color = {helper.parse_rgb(config.color or "#ffffff")}
|
||||||
|
|
||||||
|
local a = anims.Area(x2 - x1, y2 - y1)
|
||||||
|
|
||||||
|
local S = starts
|
||||||
|
local E = ends
|
||||||
|
|
||||||
|
local function text(...)
|
||||||
|
return a.add(anims.moving_font(S, E, font, ...))
|
||||||
|
end
|
||||||
|
|
||||||
|
local x, y = 0, 0
|
||||||
|
|
||||||
|
local time_size = font_size
|
||||||
|
local title_size = font_size
|
||||||
|
local abstract_size = math.floor(font_size * 0.8)
|
||||||
|
local speaker_size = math.floor(font_size * 0.8)
|
||||||
|
|
||||||
|
local current_talk = room_next_talks[1]
|
||||||
|
|
||||||
|
local col1 = 0
|
||||||
|
local col2 = 0 + font:width("in XXX min", time_size)
|
||||||
|
|
||||||
|
if #schedule == 0 then
|
||||||
|
text(col2, y, "Fetching talks...", time_size, rgba(default_color,1))
|
||||||
|
elseif not current_talk then
|
||||||
|
text(col2, y, "Nope. That's it.", time_size, rgba(default_color,1))
|
||||||
|
else
|
||||||
|
-- Time
|
||||||
|
text(col1, y, current_talk.start_str, time_size, rgba(default_color,1))
|
||||||
|
|
||||||
|
-- Delta
|
||||||
|
local delta = current_talk.start_ts - time
|
||||||
|
local talk_time
|
||||||
|
if delta > 180*60 then
|
||||||
|
talk_time = string.format("in %d h", math.floor(delta/3600))
|
||||||
|
elseif delta > 0 then
|
||||||
|
talk_time = string.format("in %d min", math.floor(delta/60)+1)
|
||||||
|
else
|
||||||
|
talk_time = "Now"
|
||||||
|
end
|
||||||
|
|
||||||
|
local y_time = y+time_size
|
||||||
|
text(col1, y_time, talk_time, time_size, rgba(default_color,1))
|
||||||
|
|
||||||
|
-- Title
|
||||||
|
local y_start = y
|
||||||
|
|
||||||
|
local lines = wrap(current_talk.title, font, title_size, a.width - col2)
|
||||||
|
for idx = 1, math.min(5, #lines) do
|
||||||
|
text(col2, y, lines[idx], title_size, rgba(default_color,1))
|
||||||
|
y = y + title_size
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_talk.track then
|
||||||
|
local r,g,b = helper.parse_rgb(current_talk.track.color)
|
||||||
|
text(col2, y, current_talk.track.name, title_size, r,g,b,1)
|
||||||
|
y = y + title_size
|
||||||
|
end
|
||||||
|
y = y + 20
|
||||||
|
|
||||||
|
-- Show abstract only if it fits into the drawing area completely
|
||||||
|
if show_abstract and a.height > (y + #lines*abstract_size + 20) then
|
||||||
|
local lines = wrap(current_talk.abstract, font, abstract_size, a.width - col2)
|
||||||
|
for idx = 1, #lines do
|
||||||
|
text(col2, y, lines[idx], abstract_size, rgba(default_color,1))
|
||||||
|
y = y + abstract_size
|
||||||
|
end
|
||||||
|
y = y + 20
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show speakers only if all of them do fit into the drawing area
|
||||||
|
if a.height > (y + #current_talk.persons*speaker_size + 20) then
|
||||||
|
for idx = 1, #current_talk.persons do
|
||||||
|
text(col2, y, current_talk.persons[idx], speaker_size, rgba(default_color,.8))
|
||||||
|
y = y + speaker_size
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for now in api.frame_between(starts, ends) do
|
||||||
|
a.draw(now, x1, y1, x2, y2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function view_all_talks(starts, ends, config, x1, y1, x2, y2)
|
||||||
|
local title_size = config.font_size or 70
|
||||||
|
local default_color = {helper.parse_rgb(config.color or "#ffffff")}
|
||||||
|
local show_speakers = config.all_speakers
|
||||||
|
|
||||||
|
local a = anims.Area(x2 - x1, y2 - y1)
|
||||||
|
|
||||||
|
local S = starts
|
||||||
|
local E = ends
|
||||||
|
|
||||||
|
local time_size = title_size
|
||||||
|
local info_size = math.floor(title_size * 0.8)
|
||||||
|
|
||||||
|
-- always leave room for 15px of track bar
|
||||||
|
local col1 = 0
|
||||||
|
local col2 = 25 + font:width("XXX min ago", time_size)
|
||||||
|
|
||||||
|
local x, y = 0, 0
|
||||||
|
|
||||||
|
local function text(...)
|
||||||
|
return a.add(anims.moving_font(S, E, font, ...))
|
||||||
|
end
|
||||||
|
|
||||||
|
if #schedule == 0 then
|
||||||
|
text(col2, y, "Fetching talks...", title_size, rgba(default_color,1))
|
||||||
|
elseif #all_next_talks == 0 and #schedule > 0 and sys.now() > 30 then
|
||||||
|
text(col2, y, "Nope. That's it.", title_size, rgba(default_color,1))
|
||||||
|
end
|
||||||
|
|
||||||
|
for idx = 1, #all_next_talks do
|
||||||
|
local talk = all_next_talks[idx]
|
||||||
|
|
||||||
|
local title_lines = wrap(
|
||||||
|
talk.title,
|
||||||
|
font, title_size, a.width - col2
|
||||||
|
)
|
||||||
|
|
||||||
|
local info_line = talk.room
|
||||||
|
|
||||||
|
if show_speakers and #talk.persons then
|
||||||
|
info_line = info_line .. table.concat(talk.persons, ", ")
|
||||||
|
end
|
||||||
|
|
||||||
|
local info_lines = wrap(
|
||||||
|
info_line,
|
||||||
|
font, info_size, a.width - col2
|
||||||
|
)
|
||||||
|
|
||||||
|
if y + #title_lines * title_size + 3 + #info_lines * info_size > a.height then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- time
|
||||||
|
local talk_time
|
||||||
|
local delta = talk.start_ts - time
|
||||||
|
if delta > -60 and delta < 60 then
|
||||||
|
talk_time = "Now"
|
||||||
|
elseif delta > 180*60 then
|
||||||
|
talk_time = talk.start_str
|
||||||
|
elseif delta > 0 then
|
||||||
|
talk_time = string.format("in %d min", math.floor(delta/60)+1)
|
||||||
|
else
|
||||||
|
talk_time = string.format("%d min ago", math.ceil(-delta/60))
|
||||||
|
end
|
||||||
|
text(col1, y, talk_time, time_size, rgba(default_color, 1))
|
||||||
|
|
||||||
|
if show_track and talk.track then
|
||||||
|
local r,g,b = helper.parse_rgb(talk.track.color)
|
||||||
|
a.add(anims.moving_image_raw(
|
||||||
|
S, E, resource.create_colored_texture(r,g,b,1),
|
||||||
|
col2 - 25, y,
|
||||||
|
col2 - 10, y + #title_lines*title_size + 3 + #info_lines*info_size
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- title
|
||||||
|
for idx = 1, #title_lines do
|
||||||
|
text(col2, y, title_lines[idx], title_size, rgba(default_color,1))
|
||||||
|
y = y + title_size
|
||||||
|
end
|
||||||
|
y = y + 3
|
||||||
|
|
||||||
|
-- info
|
||||||
|
for idx = 1, #info_lines do
|
||||||
|
text(col2, y, info_lines[idx], info_size, rgba(default_color,.8))
|
||||||
|
y = y + info_size
|
||||||
|
end
|
||||||
|
y = y + 20
|
||||||
|
end
|
||||||
|
|
||||||
|
for now in api.frame_between(starts, ends) do
|
||||||
|
a.draw(now, x1, y1, x2, y2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function view_room(starts, ends, config, x1, y1, x2, y2)
|
||||||
|
local font_size = config.font_size or 70
|
||||||
|
local r,g,b = helper.parse_rgb(config.color or "#ffffff")
|
||||||
|
|
||||||
|
for now in api.frame_between(starts, ends) do
|
||||||
|
local line = current_room
|
||||||
|
font:write(x1, y1, line, font_size, r,g,b)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function view_day(starts, ends, config, x1, y1, x2, y2)
|
||||||
|
local font_size = config.font_size or 70
|
||||||
|
local r,g,b = helper.parse_rgb(config.color or "#ffffff")
|
||||||
|
local align = config.day_align or "left"
|
||||||
|
local template = config.day_template or "Day %d"
|
||||||
|
|
||||||
|
for now in api.frame_between(starts, ends) do
|
||||||
|
local line = string.format(template, day)
|
||||||
|
if align == "left" then
|
||||||
|
font:write(x1, y1, line, font_size, r,g,b)
|
||||||
|
elseif align == "center" then
|
||||||
|
local w = font:width(line, font_size)
|
||||||
|
font:write((x1+x2-w) / 2, y1, line, font_size, r,g,b)
|
||||||
|
else
|
||||||
|
local w = font:width(line, font_size)
|
||||||
|
font:write(x2-w, y1, line, font_size, r,g,b)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.task(starts, ends, config, x1, y1, x2, y2)
|
||||||
|
check_next_talks()
|
||||||
|
return ({
|
||||||
|
next_talk = view_next_talk,
|
||||||
|
all_talks = view_all_talks,
|
||||||
|
|
||||||
|
room = view_room,
|
||||||
|
day = view_day,
|
||||||
|
})[config.mode or 'all_talks'](starts, ends, config, x1, y1, x2, y2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.can_show(config)
|
||||||
|
local mode = config.mode or 'all_talks'
|
||||||
|
-- these can always play
|
||||||
|
if mode == "day" or
|
||||||
|
mode == "all_talks"
|
||||||
|
then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return not not current_room
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
Loading…
Reference in a new issue