class Application < Lively::Application

Inherits from
Lively::Application

Represents the main Presently application middleware.

Routes the presentation, recording, playback, export, and live interfaces. Creates a shared class Presently::PresentationController that keeps connected clients in sync.

Definitions

def initialize(delegate, slides_root: "slides", templates_roots: [], recordings_root: nil, playback_recordings_root: nil)

Initialize a new Presently application.

Signature

parameter delegate Protocol::HTTP::Middleware

The next middleware in the chain.

parameter slides_root String

The directory containing slide files.

parameter templates_roots Array(String)

Additional directories to search for templates.

parameter recordings_root String | Nil

Directory where source narration is stored. Defaults to audio beside the slides directory.

parameter playback_recordings_root String | Nil

Directory containing normalized narration. Defaults to audio-normalized beside the slides directory.

Implementation

def initialize(delegate, slides_root: "slides", templates_roots: [], recordings_root: nil, playback_recordings_root: nil)
	@slides_root = slides_root
	@templates_roots = templates_roots
	@recordings = Recordings.new(recordings_root || File.expand_path("../audio", slides_root))
	@playback_recordings = Recordings.new(playback_recordings_root || File.expand_path("../audio-normalized", slides_root))
	
	slide_assets = SlideAssets.new(delegate, root: @slides_root, stylesheets: ->{controller.presentation.stylesheets})
	super(slide_assets)
end

def allowed_views

The view classes that this application allows.

Signature

returns Array(Class)

The allowed view classes.

Implementation

def allowed_views
	[HomeView, DisplayView, PresenterView, RecordingView]
end

def state

The shared state passed to all views via the resolver.

Signature

returns Hash

The controller as keyword state.

Implementation

def state
	{controller: controller}
end

def controller

The shared presentation controller.

Signature

returns PresentationController

The controller instance.

Implementation

def controller
	@controller ||= begin
		templates = Templates.for(@templates_roots)
		presentation = Presentation.load(@slides_root, templates)
		
		PresentationController.new(presentation, state: State.new)
	end
end

def title

The application title shown in the browser.

Signature

returns String

The page title.

Implementation

def title
	"Presently"
end

def make_page(view)

Create a Presently page with the presentation-specific stylesheets.

Signature

parameter view Live::View

The root view for the page.

returns Page

The presentation page.

Implementation

def make_page(view)
	stylesheets = controller.presentation.stylesheets.map(&:url)
	Page.new(title: title, body: view, stylesheets: stylesheets)
end

def configure_routes(router)

Add Presently's application routes.

Signature

parameter router Lively::Router::Builder

The router to configure.

Implementation

def configure_routes(router)
	router.get("/") do
		body = resolver.root(HomeView)
		Page.new(title: title, body: body).call
	end
	
	router.get("/display"){make_page(resolver.root(DisplayView)).call}
	router.get("/presenter"){make_page(resolver.root(PresenterView)).call}
	router.get("/record"){make_page(resolver.root(RecordingView)).call}
	
	router.route("/recordings", methods: ["GET", "HEAD", "PUT"]) do |request|
		handle_recording(request, request_parameters(request))
	end
	
	router.route("/playback/recordings", methods: ["GET", "HEAD"]) do |request|
		handle_playback_recording(request, request_parameters(request))
	end
	
	router.get("/playback") do |request|
		render_playback(request_parameters(request))
	end
	
	router.get("/export") do |request|
		render_export(request_parameters(request))
	end
end

def request_parameters(request)

Parse query parameters from the incoming request target.

Signature

parameter request Protocol::HTTP::Request

The incoming request.

returns Hash

The decoded query parameters.

Implementation

def request_parameters(request)
	Protocol::URL::Reference[request.path].parse_query!
end

def render_playback(parameters)

Render the narrated playback interface.

Implementation

def render_playback(parameters)
	presentation = Presentation.load(@slides_root, controller.templates)
	recording_urls = presentation.slides.each_index.map do |index|
		slide = presentation.slides[index]
		if @playback_recordings.exist?(slide) || @recordings.exist?(slide)
			"/playback/recordings?index=#{index}"
		end
	end
	
	playback = Playback.new(
		presentation: presentation,
		recording_urls: recording_urls,
		**Playback.options_from_parameters(parameters),
	)
	
	Protocol::HTTP::Response[200, [["content-type", "text/html"]], [playback.call]]
end

def render_export(parameters)

Render the printable export interface.

Implementation

def render_export(parameters)
	presentation = Presentation.load(@slides_root, controller.templates)
	export = Export.new(presentation: presentation, **Export.options_from_parameters(parameters))
	
	Protocol::HTTP::Response[200, [["content-type", "text/html"]], [export.call]]
end

def handle_recording(request, parameters)

Handle reading and writing a slide recording.

Signature

parameter request Protocol::HTTP::Request

The incoming request.

parameter parameters Hash

The decoded query parameters.

returns Protocol::HTTP::Response

Implementation

def handle_recording(request, parameters)
	index = recording_index(parameters)
	return Protocol::HTTP::Response[400, [], ["A valid slide index is required."]] unless index
	
	slide = controller.slides[index]
	return Protocol::HTTP::Response[404, [], ["Slide not found."]] unless slide
	
	case request.method
	when "GET", "HEAD"
		serve_recording(request, slide, @recordings)
	when "PUT"
		store_recording(request, slide)
	end
end

def handle_playback_recording(request, parameters)

Serve normalized narration for playback, falling back to the source take.

Implementation

def handle_playback_recording(request, parameters)
	index = recording_index(parameters)
	return Protocol::HTTP::Response[400, [], ["A valid slide index is required."]] unless index
	
	slide = controller.slides[index]
	return Protocol::HTTP::Response[404, [], ["Slide not found."]] unless slide
	
	recordings = @playback_recordings.exist?(slide) ? @playback_recordings : @recordings
	serve_recording(request, slide, recordings)
end

def recording_index(parameters)

Extract the slide index from decoded query parameters.

Signature

parameter parameters Hash

The decoded query parameters.

returns Integer | Nil

Implementation

def recording_index(parameters)
	Integer(parameters.fetch("index"))
rescue ArgumentError, KeyError
	nil
end

def serve_recording(request, slide, recordings)

Serve an existing recording.

Signature

parameter request Protocol::HTTP::Request

The incoming request.

parameter slide Slide

The requested slide.

returns Protocol::HTTP::Response

Implementation

def serve_recording(request, slide, recordings)
	unless recordings.exist?(slide)
		return Protocol::HTTP::Response[404, [], ["Recording not found."]]
	end
	
	headers = [
		["content-type", Recordings::CONTENT_TYPE],
		["cache-control", "no-store"],
	]
	body = recordings.read(slide) unless request.method == "HEAD"
	
	Protocol::HTTP::Response[200, headers, body]
end

def store_recording(request, slide)

Store an uploaded recording.

Signature

parameter request Protocol::HTTP::Request

The incoming request.

parameter slide Slide

The slide being recorded.

returns Protocol::HTTP::Response

Implementation

def store_recording(request, slide)
	content_type = request.headers["content-type"]&.split(";", 2)&.first
	unless content_type == Recordings::CONTENT_TYPE
		return Protocol::HTTP::Response[415, [], ["Expected #{Recordings::CONTENT_TYPE}."]]
	end
	
	unless request.body
		return Protocol::HTTP::Response[400, [], ["A recording body is required."]]
	end
	
	@recordings.write(slide, request.body)
	Protocol::HTTP::Response[201, [["content-type", "application/json"]], ["{\"saved\":true}"]]
rescue Recordings::TooLarge => error
	Protocol::HTTP::Response[413, [], [error.message]]
end