presently 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/presently +13 -0
- data/lib/presently/application.rb +104 -0
- data/lib/presently/clock.rb +77 -0
- data/lib/presently/display_view.rb +73 -0
- data/lib/presently/environment/application.rb +62 -0
- data/lib/presently/page.rb +38 -0
- data/lib/presently/page.xrb +31 -0
- data/lib/presently/presentation.rb +72 -0
- data/lib/presently/presentation_controller.rb +181 -0
- data/lib/presently/presenter_view.rb +264 -0
- data/lib/presently/slide.rb +148 -0
- data/lib/presently/slide_view.rb +92 -0
- data/lib/presently/state.rb +66 -0
- data/lib/presently/version.rb +9 -0
- data/lib/presently.rb +9 -0
- data/license.md +21 -0
- data/public/_components/@socketry/syntax/Syntax/CodeElement.js +337 -0
- data/public/_components/@socketry/syntax/Syntax/Errors.js +52 -0
- data/public/_components/@socketry/syntax/Syntax/Language/apache.js +49 -0
- data/public/_components/@socketry/syntax/Syntax/Language/applescript.js +157 -0
- data/public/_components/@socketry/syntax/Syntax/Language/assembly.js +42 -0
- data/public/_components/@socketry/syntax/Syntax/Language/bash-script.js +108 -0
- data/public/_components/@socketry/syntax/Syntax/Language/bash.js +32 -0
- data/public/_components/@socketry/syntax/Syntax/Language/basic.js +232 -0
- data/public/_components/@socketry/syntax/Syntax/Language/c++.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/c.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/clang.js +201 -0
- data/public/_components/@socketry/syntax/Syntax/Language/cpp.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/csharp.js +166 -0
- data/public/_components/@socketry/syntax/Syntax/Language/css.js +244 -0
- data/public/_components/@socketry/syntax/Syntax/Language/diff.js +24 -0
- data/public/_components/@socketry/syntax/Syntax/Language/go.js +135 -0
- data/public/_components/@socketry/syntax/Syntax/Language/haskell.js +110 -0
- data/public/_components/@socketry/syntax/Syntax/Language/html.js +69 -0
- data/public/_components/@socketry/syntax/Syntax/Language/io.js +68 -0
- data/public/_components/@socketry/syntax/Syntax/Language/java.js +134 -0
- data/public/_components/@socketry/syntax/Syntax/Language/javascript.js +89 -0
- data/public/_components/@socketry/syntax/Syntax/Language/json.js +36 -0
- data/public/_components/@socketry/syntax/Syntax/Language/lisp.js +38 -0
- data/public/_components/@socketry/syntax/Syntax/Language/lua.js +87 -0
- data/public/_components/@socketry/syntax/Syntax/Language/markdown.js +112 -0
- data/public/_components/@socketry/syntax/Syntax/Language/nginx.js +37 -0
- data/public/_components/@socketry/syntax/Syntax/Language/objective-c.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/ocaml.js +225 -0
- data/public/_components/@socketry/syntax/Syntax/Language/pascal.js +166 -0
- data/public/_components/@socketry/syntax/Syntax/Language/patch.js +2 -0
- data/public/_components/@socketry/syntax/Syntax/Language/perl5.js +317 -0
- data/public/_components/@socketry/syntax/Syntax/Language/php-script.js +112 -0
- data/public/_components/@socketry/syntax/Syntax/Language/php.js +18 -0
- data/public/_components/@socketry/syntax/Syntax/Language/plain.js +20 -0
- data/public/_components/@socketry/syntax/Syntax/Language/protobuf.js +77 -0
- data/public/_components/@socketry/syntax/Syntax/Language/python.js +208 -0
- data/public/_components/@socketry/syntax/Syntax/Language/ruby.js +124 -0
- data/public/_components/@socketry/syntax/Syntax/Language/scala.js +81 -0
- data/public/_components/@socketry/syntax/Syntax/Language/smalltalk.js +30 -0
- data/public/_components/@socketry/syntax/Syntax/Language/sql.js +865 -0
- data/public/_components/@socketry/syntax/Syntax/Language/super-collider.js +70 -0
- data/public/_components/@socketry/syntax/Syntax/Language/swift.js +176 -0
- data/public/_components/@socketry/syntax/Syntax/Language/xml.js +76 -0
- data/public/_components/@socketry/syntax/Syntax/Language/xrb.js +33 -0
- data/public/_components/@socketry/syntax/Syntax/Language/yaml.js +29 -0
- data/public/_components/@socketry/syntax/Syntax/Language.js +276 -0
- data/public/_components/@socketry/syntax/Syntax/Loader.js +78 -0
- data/public/_components/@socketry/syntax/Syntax/Match.js +546 -0
- data/public/_components/@socketry/syntax/Syntax/Rule.js +306 -0
- data/public/_components/@socketry/syntax/Syntax.js +356 -0
- data/public/_components/@socketry/syntax/bin/syntax-ast.js +42 -0
- data/public/_components/@socketry/syntax/examples/_template.html +53 -0
- data/public/_components/@socketry/syntax/examples/apache.html +72 -0
- data/public/_components/@socketry/syntax/examples/applescript.html +72 -0
- data/public/_components/@socketry/syntax/examples/assembly.html +74 -0
- data/public/_components/@socketry/syntax/examples/bash.html +90 -0
- data/public/_components/@socketry/syntax/examples/basic.html +87 -0
- data/public/_components/@socketry/syntax/examples/c.html +141 -0
- data/public/_components/@socketry/syntax/examples/clang.html +202 -0
- data/public/_components/@socketry/syntax/examples/csharp.html +110 -0
- data/public/_components/@socketry/syntax/examples/css-colors.html +179 -0
- data/public/_components/@socketry/syntax/examples/custom-theme.html +155 -0
- data/public/_components/@socketry/syntax/examples/diff.html +142 -0
- data/public/_components/@socketry/syntax/examples/examples.css +216 -0
- data/public/_components/@socketry/syntax/examples/go.html +413 -0
- data/public/_components/@socketry/syntax/examples/haskell.html +373 -0
- data/public/_components/@socketry/syntax/examples/html.html +316 -0
- data/public/_components/@socketry/syntax/examples/index.html +97 -0
- data/public/_components/@socketry/syntax/examples/io.html +552 -0
- data/public/_components/@socketry/syntax/examples/java.html +786 -0
- data/public/_components/@socketry/syntax/examples/javascript.html +199 -0
- data/public/_components/@socketry/syntax/examples/json.html +150 -0
- data/public/_components/@socketry/syntax/examples/lisp.html +476 -0
- data/public/_components/@socketry/syntax/examples/lua.html +737 -0
- data/public/_components/@socketry/syntax/examples/markdown.html +121 -0
- data/public/_components/@socketry/syntax/examples/mixed.html +306 -0
- data/public/_components/@socketry/syntax/examples/nginx.html +554 -0
- data/public/_components/@socketry/syntax/examples/ocaml.html +596 -0
- data/public/_components/@socketry/syntax/examples/pascal.html +762 -0
- data/public/_components/@socketry/syntax/examples/perl5.html +488 -0
- data/public/_components/@socketry/syntax/examples/php-script.html +142 -0
- data/public/_components/@socketry/syntax/examples/php.html +95 -0
- data/public/_components/@socketry/syntax/examples/plain.html +222 -0
- data/public/_components/@socketry/syntax/examples/protobuf.html +405 -0
- data/public/_components/@socketry/syntax/examples/python.html +82 -0
- data/public/_components/@socketry/syntax/examples/readme.md +79 -0
- data/public/_components/@socketry/syntax/examples/ruby.html +58 -0
- data/public/_components/@socketry/syntax/examples/scala.html +41 -0
- data/public/_components/@socketry/syntax/examples/smalltalk.html +436 -0
- data/public/_components/@socketry/syntax/examples/sql.html +373 -0
- data/public/_components/@socketry/syntax/examples/super-collider.html +55 -0
- data/public/_components/@socketry/syntax/examples/swift.html +176 -0
- data/public/_components/@socketry/syntax/examples/wrap-demo.html +103 -0
- data/public/_components/@socketry/syntax/examples/xml.html +112 -0
- data/public/_components/@socketry/syntax/examples/xrb.html +37 -0
- data/public/_components/@socketry/syntax/examples/yaml.html +72 -0
- data/public/_components/@socketry/syntax/license.md +21 -0
- data/public/_components/@socketry/syntax/package-lock.json +834 -0
- data/public/_components/@socketry/syntax/package.json +43 -0
- data/public/_components/@socketry/syntax/readme.md +162 -0
- data/public/_components/@socketry/syntax/test/Syntax/CodeElement.js +306 -0
- data/public/_components/@socketry/syntax/test/Syntax/ErrorHandling.js +85 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/apache.js +153 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/applescript.js +198 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/assembly.js +209 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/bash-script.js +225 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/bash.js +162 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/basic.js +265 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/clang.js +390 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/csharp.js +436 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/css.js +431 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/diff.js +206 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/go.js +386 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/haskell.js +454 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/html.js +111 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/io.js +229 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/java.js +362 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/javascript.js +101 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/json.js +101 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/lisp.js +224 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/lua.js +307 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/markdown.js +163 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/nginx.js +267 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/ocaml.js +299 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/pascal.js +311 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/perl5.js +333 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/php-script.js +197 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/php.js +92 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/plain.js +327 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/protobuf.js +294 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/python.js +213 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/ruby.js +70 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/scala.js +75 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/smalltalk.js +223 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/sql.js +281 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/super-collider.js +66 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/swift.js +71 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/xml.js +170 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/xrb.js +57 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language/yaml.js +123 -0
- data/public/_components/@socketry/syntax/test/Syntax/Language.js +62 -0
- data/public/_components/@socketry/syntax/test/Syntax/Match.js +40 -0
- data/public/_components/@socketry/syntax/test/Syntax/Rule.js +251 -0
- data/public/_components/@socketry/syntax/test/Syntax.js +38 -0
- data/public/_components/@socketry/syntax/test/helpers/ast-matcher.js +90 -0
- data/public/_components/@socketry/syntax/themes/base/apache.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/applescript.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/assembly.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/bash.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/basic.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/c.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/clang.css +0 -0
- data/public/_components/@socketry/syntax/themes/base/csharp.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/css.css +22 -0
- data/public/_components/@socketry/syntax/themes/base/diff.css +48 -0
- data/public/_components/@socketry/syntax/themes/base/go.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/haskell.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/html.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/io.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/java.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/javascript.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/json.css +41 -0
- data/public/_components/@socketry/syntax/themes/base/lisp.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/lua.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/markdown.css +16 -0
- data/public/_components/@socketry/syntax/themes/base/nginx.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/ocaml.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/pascal.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/perl5.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/php-script.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/php.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/plain.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/protobuf.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/python.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/ruby.css +23 -0
- data/public/_components/@socketry/syntax/themes/base/scala.css +3 -0
- data/public/_components/@socketry/syntax/themes/base/smalltalk.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/sql.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/super-collider.css +33 -0
- data/public/_components/@socketry/syntax/themes/base/swift.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/syntax.css +63 -0
- data/public/_components/@socketry/syntax/themes/base/xml.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/xrb.css +29 -0
- data/public/_components/@socketry/syntax/themes/base/yaml.css +1 -0
- data/public/_components/@socketry/syntax/themes/theming.md +233 -0
- data/public/_components/@socketry/syntax/update-examples.js +135 -0
- data/public/_static/index.css +593 -0
- data/public/application.js +147 -0
- data/readme.md +69 -0
- data/releases.md +3 -0
- data/templates/code.xrb +12 -0
- data/templates/default.xrb +5 -0
- data/templates/image.xrb +8 -0
- data/templates/section.xrb +5 -0
- data/templates/title.xrb +8 -0
- data/templates/translation.xrb +8 -0
- data/templates/two_column.xrb +8 -0
- metadata +280 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 474e433100802366e483635b1255b07c3f6b287eb5d7d5a8e9e4a4300bba02c1
|
|
4
|
+
data.tar.gz: 79318b1e1f04856ca57dcde476c1bae8921e261ab018c7d75c1d8637dbecf06e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4d8b7743fe9f90c69f69a8905a93604d251ab4f64617a319cd26e223c8a3c750a712d3ed06a5d21c1554d5cb480345a0d008e6669c18b7ced1ef9f68e0fba813
|
|
7
|
+
data.tar.gz: 016f73a329b50181cb044a2561a097c48f3b5e8b504295d25577f034c712e0199a55251436ba7475d80b10843798be823888d5c648411a3f819899c7a647e1d5
|
data/bin/presently
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "async/service"
|
|
5
|
+
require_relative "../lib/presently/environment/application"
|
|
6
|
+
|
|
7
|
+
configuration = Async::Service::Configuration.build do
|
|
8
|
+
service "presently" do
|
|
9
|
+
include Presently::Environment::Application
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
Async::Service::Controller.run(configuration)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "live"
|
|
7
|
+
require "lively"
|
|
8
|
+
|
|
9
|
+
require_relative "presentation"
|
|
10
|
+
require_relative "presentation_controller"
|
|
11
|
+
require_relative "state"
|
|
12
|
+
require_relative "display_view"
|
|
13
|
+
require_relative "presenter_view"
|
|
14
|
+
require_relative "page"
|
|
15
|
+
|
|
16
|
+
module Presently
|
|
17
|
+
# Extends {Live::Resolver} to pass shared state to views on construction.
|
|
18
|
+
#
|
|
19
|
+
# When the browser reconnects via WebSocket, the resolver creates new view
|
|
20
|
+
# instances with the shared {PresentationController} so all clients stay in sync.
|
|
21
|
+
class Resolver < Live::Resolver
|
|
22
|
+
# Initialize a new resolver with shared state.
|
|
23
|
+
# @parameter state [Hash] Key-value pairs to pass to view constructors.
|
|
24
|
+
def initialize(**state)
|
|
25
|
+
super()
|
|
26
|
+
@state = state
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @attribute [Hash] The shared state passed to view constructors.
|
|
30
|
+
attr :state
|
|
31
|
+
|
|
32
|
+
# Resolve a client-side element to a server-side instance with shared state.
|
|
33
|
+
# @parameter id [String] The unique element identifier.
|
|
34
|
+
# @parameter data [Hash] The element data attributes.
|
|
35
|
+
# @returns [Live::Element | Nil] The resolved element, or `nil`.
|
|
36
|
+
def call(id, data)
|
|
37
|
+
if klass = @allowed[data[:class]]
|
|
38
|
+
return klass.new(id, data, **@state)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The main Presently application middleware.
|
|
44
|
+
#
|
|
45
|
+
# Handles routing for the display view (`/`), presenter view (`/presenter`),
|
|
46
|
+
# and WebSocket connections (`/live`). Creates a shared {PresentationController}
|
|
47
|
+
# that keeps all connected clients in sync.
|
|
48
|
+
class Application < Lively::Application
|
|
49
|
+
# Initialize a new Presently application.
|
|
50
|
+
# @parameter delegate [Protocol::HTTP::Middleware] The next middleware in the chain.
|
|
51
|
+
# @parameter slides_root [String] The directory containing slide files.
|
|
52
|
+
# @parameter templates_root [String | Nil] The directory containing custom templates.
|
|
53
|
+
# @parameter options [Hash] Additional options passed to the parent.
|
|
54
|
+
def initialize(delegate, slides_root: "slides", templates_root: nil, **options)
|
|
55
|
+
presentation = Presentation.load(slides_root, templates_root: templates_root)
|
|
56
|
+
|
|
57
|
+
state = State.new
|
|
58
|
+
|
|
59
|
+
resolver = Resolver.new(
|
|
60
|
+
controller: PresentationController.new(presentation, state: state),
|
|
61
|
+
).tap do |resolver|
|
|
62
|
+
resolver.allow(DisplayView, PresenterView)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
super(delegate, resolver: resolver, **options)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The shared presentation controller.
|
|
69
|
+
# @returns [PresentationController] The controller instance.
|
|
70
|
+
def controller
|
|
71
|
+
resolver.state[:controller]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The application title shown in the browser.
|
|
75
|
+
# @returns [String] The page title.
|
|
76
|
+
def title
|
|
77
|
+
"Presently"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Create the body view for the given request path.
|
|
81
|
+
# @parameter request [Protocol::HTTP::Request] The incoming request.
|
|
82
|
+
# @returns [Live::View | Nil] The view for the path, or `nil` for unknown paths.
|
|
83
|
+
def body(request)
|
|
84
|
+
case request.path
|
|
85
|
+
when "/"
|
|
86
|
+
DisplayView.new(controller: controller)
|
|
87
|
+
when "/presenter"
|
|
88
|
+
PresenterView.new(controller: controller)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Handle an HTTP request by rendering the appropriate page.
|
|
93
|
+
# @parameter request [Protocol::HTTP::Request] The incoming request.
|
|
94
|
+
# @returns [Protocol::HTTP::Response] The HTTP response.
|
|
95
|
+
def handle(request)
|
|
96
|
+
if body = self.body(request)
|
|
97
|
+
page = Page.new(title: title, body: body)
|
|
98
|
+
return Protocol::HTTP::Response[200, [], [page.call]]
|
|
99
|
+
else
|
|
100
|
+
return Protocol::HTTP::Response[404, [], ["Not Found"]]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Presently
|
|
7
|
+
# A simple clock that tracks elapsed time with start, pause, resume, and reset.
|
|
8
|
+
#
|
|
9
|
+
# The clock accumulates elapsed time while running and freezes it when paused.
|
|
10
|
+
class Clock
|
|
11
|
+
# Initialize a new clock in the stopped state.
|
|
12
|
+
def initialize
|
|
13
|
+
@elapsed = 0
|
|
14
|
+
@running = false
|
|
15
|
+
@last_tick = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Whether the clock has been started at least once.
|
|
19
|
+
# @returns [Boolean]
|
|
20
|
+
def started?
|
|
21
|
+
!@last_tick.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Whether the clock is currently running and accumulating time.
|
|
25
|
+
# @returns [Boolean]
|
|
26
|
+
def running?
|
|
27
|
+
@running
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Whether the clock has been started but is currently paused.
|
|
31
|
+
# @returns [Boolean]
|
|
32
|
+
def paused?
|
|
33
|
+
started? && !@running
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The total elapsed time in seconds.
|
|
37
|
+
# Includes time accumulated up to now if running, or frozen time if paused.
|
|
38
|
+
# @returns [Numeric] The elapsed time in seconds.
|
|
39
|
+
def elapsed
|
|
40
|
+
if @running
|
|
41
|
+
@elapsed + (Time.now - @last_tick)
|
|
42
|
+
else
|
|
43
|
+
@elapsed
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Start the clock. Begins accumulating time from now.
|
|
48
|
+
def start!
|
|
49
|
+
@running = true
|
|
50
|
+
@last_tick = Time.now
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Pause the clock. Freezes the elapsed time at the current value.
|
|
54
|
+
def pause!
|
|
55
|
+
return unless @running
|
|
56
|
+
|
|
57
|
+
@elapsed += Time.now - @last_tick
|
|
58
|
+
@running = false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Resume the clock after a pause. Continues accumulating time from now.
|
|
62
|
+
def resume!
|
|
63
|
+
return if @running
|
|
64
|
+
|
|
65
|
+
@running = true
|
|
66
|
+
@last_tick = Time.now
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Reset the elapsed time to the given value.
|
|
70
|
+
# If running, continues from the new value. If paused, sets the frozen value.
|
|
71
|
+
# @parameter elapsed [Numeric] The new elapsed time in seconds.
|
|
72
|
+
def reset!(elapsed = 0)
|
|
73
|
+
@elapsed = elapsed
|
|
74
|
+
@last_tick = Time.now if @running
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "live"
|
|
7
|
+
require_relative "slide_view"
|
|
8
|
+
|
|
9
|
+
module Presently
|
|
10
|
+
# The audience-facing display view that renders the current slide full-screen.
|
|
11
|
+
#
|
|
12
|
+
# Connects to the {PresentationController} as a listener and updates
|
|
13
|
+
# whenever the slide changes. Pushes the current state on WebSocket reconnect.
|
|
14
|
+
class DisplayView < Live::View
|
|
15
|
+
# Initialize a new display view.
|
|
16
|
+
# @parameter id [String] The unique element identifier.
|
|
17
|
+
# @parameter data [Hash] The element data attributes.
|
|
18
|
+
# @parameter controller [PresentationController | Nil] The shared presentation controller.
|
|
19
|
+
def initialize(id = Live::Element.unique_id, data = {}, controller: nil)
|
|
20
|
+
super(id, data)
|
|
21
|
+
@controller = controller
|
|
22
|
+
@slide_renderer = SlideView.new(css_class: "slide current", controller: controller)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Bind this view to a page and register as a listener.
|
|
26
|
+
# Immediately pushes the current state to the client.
|
|
27
|
+
# @parameter page [Live::Page] The page this view is bound to.
|
|
28
|
+
def bind(page)
|
|
29
|
+
super
|
|
30
|
+
@controller.add_listener(self)
|
|
31
|
+
self.update!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Close this view and unregister as a listener.
|
|
35
|
+
def close
|
|
36
|
+
@controller.remove_listener(self)
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Called by the controller when the slide changes.
|
|
41
|
+
def slide_changed!
|
|
42
|
+
self.update!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Handle an event from the client.
|
|
46
|
+
# @parameter event [Hash] The event data with `:detail` containing the action.
|
|
47
|
+
def handle(event)
|
|
48
|
+
case event.dig(:detail, :action)
|
|
49
|
+
when "next"
|
|
50
|
+
@controller.advance!
|
|
51
|
+
when "previous"
|
|
52
|
+
@controller.retreat!
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Render the display view.
|
|
57
|
+
# @parameter builder [XRB::Builder] The HTML builder.
|
|
58
|
+
def render(builder)
|
|
59
|
+
slide = @controller.current_slide
|
|
60
|
+
return unless slide
|
|
61
|
+
|
|
62
|
+
builder.tag(:div, class: "display", data: {transition: slide.transition}) do
|
|
63
|
+
builder.tag(:div, class: "slide-container") do
|
|
64
|
+
@slide_renderer.render_slide(builder, slide)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
builder.tag(:div, class: "slide-counter") do
|
|
68
|
+
builder.text("#{@controller.current_index + 1} / #{@controller.slide_count}")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../application"
|
|
7
|
+
require "lively/environment/application"
|
|
8
|
+
|
|
9
|
+
module Presently
|
|
10
|
+
module Environment
|
|
11
|
+
# The environment configuration for a Presently application server.
|
|
12
|
+
#
|
|
13
|
+
# Extends the Lively environment with Presently-specific middleware and configuration.
|
|
14
|
+
# Override {#slides_root} and {#templates_root} to customize paths.
|
|
15
|
+
module Application
|
|
16
|
+
include Lively::Environment::Application
|
|
17
|
+
|
|
18
|
+
# The root directory containing slide Markdown files.
|
|
19
|
+
# @returns [String] Absolute path to the slides root.
|
|
20
|
+
def slides_root
|
|
21
|
+
File.expand_path("slides", self.root)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The root directory containing slide templates.
|
|
25
|
+
# Defaults to the gem's bundled templates.
|
|
26
|
+
# @returns [String] Absolute path to the templates root.
|
|
27
|
+
def templates_root
|
|
28
|
+
File.expand_path("../../../templates", __dir__)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The application class to use.
|
|
32
|
+
# @returns [Class] The Presently application class.
|
|
33
|
+
def application
|
|
34
|
+
Presently::Application
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Build the middleware stack with Presently's public assets.
|
|
38
|
+
# @returns [Protocol::HTTP::Middleware] The complete middleware stack.
|
|
39
|
+
def middleware
|
|
40
|
+
application = self.application
|
|
41
|
+
slides_root = self.slides_root
|
|
42
|
+
templates_root = self.templates_root
|
|
43
|
+
root = self.root
|
|
44
|
+
|
|
45
|
+
::Protocol::HTTP::Middleware.build do |builder|
|
|
46
|
+
# Serve assets from the user's public directory:
|
|
47
|
+
builder.use Lively::Assets, root: File.expand_path("public", root)
|
|
48
|
+
|
|
49
|
+
# Serve Presently's bundled assets (syntax-js, CSS, etc.):
|
|
50
|
+
builder.use Lively::Assets, root: File.expand_path("../../../public", __dir__)
|
|
51
|
+
|
|
52
|
+
# Serve Lively's built-in assets (Live.js, morphdom, etc.):
|
|
53
|
+
builder.use Lively::Assets, root: File.expand_path("public", Gem.loaded_specs["lively"].full_gem_path)
|
|
54
|
+
|
|
55
|
+
builder.use application,
|
|
56
|
+
slides_root: slides_root,
|
|
57
|
+
templates_root: templates_root
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "xrb/template"
|
|
7
|
+
|
|
8
|
+
module Presently
|
|
9
|
+
# The HTML page shell for a Presently view.
|
|
10
|
+
#
|
|
11
|
+
# Renders the initial HTML page with the import map, stylesheets, and the
|
|
12
|
+
# embedded Live view component. Uses a custom XRB template that includes
|
|
13
|
+
# Presently's assets (syntax highlighting, etc.).
|
|
14
|
+
class Page
|
|
15
|
+
# The compiled XRB template for the page shell.
|
|
16
|
+
TEMPLATE = XRB::Template.load_file(File.expand_path("page.xrb", __dir__))
|
|
17
|
+
|
|
18
|
+
# Initialize a new page.
|
|
19
|
+
# @parameter title [String] The page title.
|
|
20
|
+
# @parameter body [Live::View | Nil] The Live view to embed in the page.
|
|
21
|
+
def initialize(title: "Presently", body: nil)
|
|
22
|
+
@title = title
|
|
23
|
+
@body = body
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @attribute [String] The page title.
|
|
27
|
+
attr :title
|
|
28
|
+
|
|
29
|
+
# @attribute [Live::View | Nil] The Live view to embed.
|
|
30
|
+
attr :body
|
|
31
|
+
|
|
32
|
+
# Render the page to an HTML string.
|
|
33
|
+
# @returns [String] The rendered HTML.
|
|
34
|
+
def call
|
|
35
|
+
TEMPLATE.to_string(self)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>#{self.title}</title>
|
|
5
|
+
|
|
6
|
+
<meta charset="UTF-8" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
|
|
9
|
+
<link rel="icon" type="image/png" href="/_static/icon.png" />
|
|
10
|
+
<link rel="stylesheet" href="/_static/site.css" type="text/css" media="screen" />
|
|
11
|
+
<link rel="stylesheet" href="/_static/index.css" type="text/css" media="screen" />
|
|
12
|
+
<link rel="stylesheet" href="/_components/@socketry/syntax/themes/base/syntax.css" type="text/css" media="screen" />
|
|
13
|
+
|
|
14
|
+
<script type="importmap">
|
|
15
|
+
{
|
|
16
|
+
"imports": {
|
|
17
|
+
"live": "/_components/@socketry/live/Live.js",
|
|
18
|
+
"live-audio": "/_components/@socketry/live-audio/Live/Audio.js",
|
|
19
|
+
"morphdom": "/_components/morphdom/morphdom-esm.js",
|
|
20
|
+
"@socketry/syntax": "/_components/@socketry/syntax/Syntax.js"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<script type="module" src="/application.js"></script>
|
|
26
|
+
</head>
|
|
27
|
+
|
|
28
|
+
<body>
|
|
29
|
+
#{self.body&.to_html || "No body specified!"}
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "slide"
|
|
7
|
+
|
|
8
|
+
module Presently
|
|
9
|
+
# An immutable collection of slides with configuration.
|
|
10
|
+
#
|
|
11
|
+
# Use {.load} to create a presentation from a directory of Markdown files,
|
|
12
|
+
# or initialize directly with an array of {Slide} instances.
|
|
13
|
+
class Presentation
|
|
14
|
+
# Load a presentation from a directory of Markdown slide files.
|
|
15
|
+
# @parameter slides_root [String] The directory containing `.md` slide files.
|
|
16
|
+
# @parameter options [Hash] Additional options passed to {#initialize}.
|
|
17
|
+
# @returns [Presentation] A new presentation with slides loaded from the directory.
|
|
18
|
+
def self.load(slides_root = "slides", **options)
|
|
19
|
+
pattern = File.join(slides_root, "*.md")
|
|
20
|
+
slides = Dir.glob(pattern).sort.map{|path| Slide.new(path)}
|
|
21
|
+
|
|
22
|
+
new(slides, slides_root: slides_root, **options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Initialize a new presentation.
|
|
26
|
+
# @parameter slides [Array(Slide)] The ordered list of slides.
|
|
27
|
+
# @parameter slides_root [String | Nil] The directory slides were loaded from, used by {#reload!}.
|
|
28
|
+
# @parameter templates_root [String | Nil] The directory containing custom `.xrb` templates.
|
|
29
|
+
def initialize(slides = [], slides_root: nil, templates_root: nil)
|
|
30
|
+
@slides = slides
|
|
31
|
+
@slides_root = slides_root
|
|
32
|
+
@templates_root = templates_root
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @attribute [Array(Slide)] The ordered list of slides.
|
|
36
|
+
attr :slides
|
|
37
|
+
|
|
38
|
+
# @attribute [String | Nil] The directory slides were loaded from.
|
|
39
|
+
attr :slides_root
|
|
40
|
+
|
|
41
|
+
# @attribute [String | Nil] The directory containing custom templates.
|
|
42
|
+
attr :templates_root
|
|
43
|
+
|
|
44
|
+
# The number of slides in the presentation.
|
|
45
|
+
# @returns [Integer] The slide count.
|
|
46
|
+
def slide_count
|
|
47
|
+
@slides.length
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The total expected duration of the presentation in seconds.
|
|
51
|
+
# @returns [Numeric] The sum of all slide durations.
|
|
52
|
+
def total_duration
|
|
53
|
+
@slides.sum(&:duration)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Calculate the expected elapsed time for slides up to the given index.
|
|
57
|
+
# @parameter index [Integer] The slide index (exclusive).
|
|
58
|
+
# @returns [Numeric] The sum of durations for slides before the given index.
|
|
59
|
+
def expected_time_at(index)
|
|
60
|
+
@slides[0...index].sum(&:duration)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Reload slides from the original directory.
|
|
64
|
+
# Only works if the presentation was created with {.load}.
|
|
65
|
+
def reload!
|
|
66
|
+
if @slides_root
|
|
67
|
+
pattern = File.join(@slides_root, "*.md")
|
|
68
|
+
@slides = Dir.glob(pattern).sort.map{|path| Slide.new(path)}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "clock"
|
|
7
|
+
require_relative "presentation"
|
|
8
|
+
require_relative "state"
|
|
9
|
+
|
|
10
|
+
module Presently
|
|
11
|
+
# Manages the mutable state of a presentation: current slide, clock, and listeners.
|
|
12
|
+
#
|
|
13
|
+
# Wraps an immutable {Presentation} and provides navigation, timing, and listener notification.
|
|
14
|
+
# Multiple views (display, presenter) can register as listeners to receive updates.
|
|
15
|
+
class PresentationController
|
|
16
|
+
# Initialize a new controller for the given presentation.
|
|
17
|
+
# @parameter presentation [Presentation] The presentation to control.
|
|
18
|
+
# @parameter state [State | Nil] The state persistence object. If provided, state is saved on changes and restored on initialization.
|
|
19
|
+
def initialize(presentation, state: nil)
|
|
20
|
+
@presentation = presentation
|
|
21
|
+
@current_index = 0
|
|
22
|
+
@clock = Clock.new
|
|
23
|
+
@listeners = []
|
|
24
|
+
@state = state
|
|
25
|
+
|
|
26
|
+
@state&.load(self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @attribute [Presentation] The underlying presentation data.
|
|
30
|
+
attr :presentation
|
|
31
|
+
|
|
32
|
+
# @attribute [Integer] The index of the current slide.
|
|
33
|
+
attr :current_index
|
|
34
|
+
|
|
35
|
+
# @attribute [Clock] The presentation timer.
|
|
36
|
+
attr :clock
|
|
37
|
+
|
|
38
|
+
# The templates root directory, delegated to the presentation.
|
|
39
|
+
# @returns [String | Nil] The templates root path.
|
|
40
|
+
def templates_root
|
|
41
|
+
@presentation.templates_root
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The ordered list of slides, delegated to the presentation.
|
|
45
|
+
# @returns [Array(Slide)] The slides.
|
|
46
|
+
def slides
|
|
47
|
+
@presentation.slides
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The currently displayed slide.
|
|
51
|
+
# @returns [Slide | Nil] The current slide, or `nil` if no slides are loaded.
|
|
52
|
+
def current_slide
|
|
53
|
+
@presentation.slides[@current_index]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The slide following the current one.
|
|
57
|
+
# @returns [Slide | Nil] The next slide, or `nil` if on the last slide.
|
|
58
|
+
def next_slide
|
|
59
|
+
@presentation.slides[@current_index + 1]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The slide preceding the current one.
|
|
63
|
+
# @returns [Slide | Nil] The previous slide, or `nil` if on the first slide.
|
|
64
|
+
def previous_slide
|
|
65
|
+
@presentation.slides[@current_index - 1] if @current_index > 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The total number of slides.
|
|
69
|
+
# @returns [Integer] The slide count.
|
|
70
|
+
def slide_count
|
|
71
|
+
@presentation.slide_count
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The total expected duration of the presentation.
|
|
75
|
+
# @returns [Numeric] The total duration in seconds.
|
|
76
|
+
def total_duration
|
|
77
|
+
@presentation.total_duration
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The progress through the current slide's allocated time.
|
|
81
|
+
# @returns [Float] A value between 0.0 and 1.0.
|
|
82
|
+
def slide_progress
|
|
83
|
+
return 0.0 unless @clock.started?
|
|
84
|
+
|
|
85
|
+
slide = current_slide
|
|
86
|
+
return 0.0 unless slide
|
|
87
|
+
|
|
88
|
+
time_into_slide = @clock.elapsed - @presentation.expected_time_at(@current_index)
|
|
89
|
+
(time_into_slide / slide.duration).clamp(0.0, 1.0)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Reset the timer so that elapsed time matches the expected time for the current slide.
|
|
93
|
+
def reset_timer!
|
|
94
|
+
@clock.reset!(@presentation.expected_time_at(@current_index))
|
|
95
|
+
notify_listeners!
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The current pacing status relative to the slide timing.
|
|
99
|
+
# @returns [Symbol] One of `:on_time`, `:ahead`, or `:behind`.
|
|
100
|
+
def pacing
|
|
101
|
+
return :on_time unless @clock.started?
|
|
102
|
+
|
|
103
|
+
elapsed = @clock.elapsed
|
|
104
|
+
slide_start = @presentation.expected_time_at(@current_index)
|
|
105
|
+
slide_end = @presentation.expected_time_at(@current_index + 1)
|
|
106
|
+
|
|
107
|
+
if elapsed > slide_end
|
|
108
|
+
:behind
|
|
109
|
+
elsif elapsed < slide_start
|
|
110
|
+
:ahead
|
|
111
|
+
else
|
|
112
|
+
:on_time
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The estimated time remaining in the presentation.
|
|
117
|
+
# @returns [Numeric] The remaining time in seconds.
|
|
118
|
+
def time_remaining
|
|
119
|
+
return total_duration unless @clock.started?
|
|
120
|
+
|
|
121
|
+
expected_remaining = @presentation.expected_time_at(slide_count) - @clock.elapsed
|
|
122
|
+
|
|
123
|
+
[expected_remaining, 0].max
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Navigate to a specific slide by index.
|
|
127
|
+
# Ignores out-of-bounds indices. Notifies listeners on change.
|
|
128
|
+
# @parameter index [Integer] The slide index to navigate to.
|
|
129
|
+
def go_to(index)
|
|
130
|
+
return if index < 0 || index >= slide_count
|
|
131
|
+
|
|
132
|
+
@current_index = index
|
|
133
|
+
notify_listeners!
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Advance to the next slide.
|
|
137
|
+
def advance!
|
|
138
|
+
go_to(@current_index + 1)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Go back to the previous slide.
|
|
142
|
+
def retreat!
|
|
143
|
+
go_to(@current_index - 1)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Register a listener to be notified when the slide changes.
|
|
147
|
+
# The listener must respond to `#slide_changed!`.
|
|
148
|
+
# @parameter listener [Object] The listener to add.
|
|
149
|
+
def add_listener(listener)
|
|
150
|
+
@listeners << listener
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Remove a previously registered listener.
|
|
154
|
+
# @parameter listener [Object] The listener to remove.
|
|
155
|
+
def remove_listener(listener)
|
|
156
|
+
@listeners.delete(listener)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Reload slides from disk and notify listeners.
|
|
160
|
+
def reload!
|
|
161
|
+
@presentation.reload!
|
|
162
|
+
notify_listeners!
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Persist the current state to disk.
|
|
166
|
+
def save_state!
|
|
167
|
+
@state&.save(self)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
# Notify all registered listeners that the slide has changed, and persist state.
|
|
173
|
+
def notify_listeners!
|
|
174
|
+
@state&.save(self)
|
|
175
|
+
|
|
176
|
+
@listeners.each do |listener|
|
|
177
|
+
listener.slide_changed! rescue nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|