its-showtime 0.1.1
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/LICENSE +21 -0
- data/README.md +179 -0
- data/bin/showtime +47 -0
- data/lib/showtime/app.rb +399 -0
- data/lib/showtime/charts.rb +229 -0
- data/lib/showtime/component_registry.rb +38 -0
- data/lib/showtime/components/Components.md +309 -0
- data/lib/showtime/components/alerts.rb +83 -0
- data/lib/showtime/components/base.rb +63 -0
- data/lib/showtime/components/charts.rb +119 -0
- data/lib/showtime/components/data.rb +328 -0
- data/lib/showtime/components/inputs.rb +390 -0
- data/lib/showtime/components/layout.rb +135 -0
- data/lib/showtime/components/media.rb +73 -0
- data/lib/showtime/components/sidebar.rb +130 -0
- data/lib/showtime/components/text.rb +156 -0
- data/lib/showtime/components.rb +18 -0
- data/lib/showtime/compute_tracker.rb +21 -0
- data/lib/showtime/helpers.rb +53 -0
- data/lib/showtime/logger.rb +143 -0
- data/lib/showtime/public/.vite/manifest.json +34 -0
- data/lib/showtime/public/assets/antd-3aDVoXqG.js +447 -0
- data/lib/showtime/public/assets/charts-iowb_sWQ.js +3858 -0
- data/lib/showtime/public/assets/index-B2b3lWS5.js +43 -0
- data/lib/showtime/public/assets/index-M6NVamDM.css +1 -0
- data/lib/showtime/public/assets/react-BE6xecJX.js +32 -0
- data/lib/showtime/public/index.html +19 -0
- data/lib/showtime/public/letter.png +0 -0
- data/lib/showtime/public/logo.png +0 -0
- data/lib/showtime/release.rb +108 -0
- data/lib/showtime/session.rb +131 -0
- data/lib/showtime/version.rb +3 -0
- data/lib/showtime/views/index.erb +32 -0
- data/lib/showtime.rb +157 -0
- metadata +300 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'ostruct'
|
|
5
|
+
|
|
6
|
+
module Showtime
|
|
7
|
+
module Components
|
|
8
|
+
class Button < BaseComponent
|
|
9
|
+
attr_reader :label, :value
|
|
10
|
+
|
|
11
|
+
def initialize(label, key: nil, help: nil)
|
|
12
|
+
super(key: key, help: help)
|
|
13
|
+
@label = label
|
|
14
|
+
|
|
15
|
+
# Initialize button state
|
|
16
|
+
@value = St.get(@key, false)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def clicked?
|
|
20
|
+
St.get(@key) == true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def value
|
|
24
|
+
St.get(@key) == true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
# Get the latest value from the session
|
|
29
|
+
@value = St.get(@key) == true
|
|
30
|
+
|
|
31
|
+
super.merge({
|
|
32
|
+
label: @label,
|
|
33
|
+
value: @value
|
|
34
|
+
})
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Checkbox < BaseComponent
|
|
39
|
+
attr_reader :label, :value
|
|
40
|
+
|
|
41
|
+
def initialize(label, value: false, key: nil, help: nil)
|
|
42
|
+
super(key: key, help: help)
|
|
43
|
+
@label = label
|
|
44
|
+
@value = value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
super.merge({
|
|
49
|
+
label: @label,
|
|
50
|
+
value: @value
|
|
51
|
+
})
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class TextInput < BaseComponent
|
|
56
|
+
attr_reader :label
|
|
57
|
+
|
|
58
|
+
def initialize(label, value: "", key: nil, help: nil)
|
|
59
|
+
super(key: key, help: help)
|
|
60
|
+
@label = label
|
|
61
|
+
@value = value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_h
|
|
65
|
+
super.merge({
|
|
66
|
+
label: @label,
|
|
67
|
+
value: value
|
|
68
|
+
})
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class NumberInput < BaseComponent
|
|
73
|
+
attr_reader :label, :min_value, :max_value, :step
|
|
74
|
+
|
|
75
|
+
def initialize(label, min_value: nil, max_value: nil, value: nil, step: 1, key: nil, help: nil)
|
|
76
|
+
super(key: key, help: help)
|
|
77
|
+
@label = label
|
|
78
|
+
@min_value = min_value
|
|
79
|
+
@max_value = max_value
|
|
80
|
+
@value = value || min_value || 0
|
|
81
|
+
@step = step
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_h
|
|
85
|
+
super.merge({
|
|
86
|
+
label: @label,
|
|
87
|
+
min_value: @min_value,
|
|
88
|
+
max_value: @max_value,
|
|
89
|
+
value: value,
|
|
90
|
+
step: @step
|
|
91
|
+
})
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class TextArea < BaseComponent
|
|
96
|
+
attr_reader :label, :height
|
|
97
|
+
|
|
98
|
+
def initialize(label, value: "", height: nil, key: nil, help: nil, **kwargs)
|
|
99
|
+
super(key: key, help: help)
|
|
100
|
+
@label = label
|
|
101
|
+
# Ensure @value is initialized even if not using session or no initial value in session
|
|
102
|
+
@value = St.get(@key) || value
|
|
103
|
+
@height = height
|
|
104
|
+
@kwargs = kwargs
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def to_h
|
|
108
|
+
super.merge({
|
|
109
|
+
label: @label,
|
|
110
|
+
value: value, # Use the new value getter
|
|
111
|
+
height: @height
|
|
112
|
+
}).merge(@kwargs)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class DateInput < BaseComponent
|
|
117
|
+
attr_reader :label, :value, :min_value, :max_value, :format
|
|
118
|
+
|
|
119
|
+
def initialize(label, value: nil, min_value: nil, max_value: nil, format: 'YYYY-MM-DD', key: nil, help: nil)
|
|
120
|
+
super(key: key, help: help)
|
|
121
|
+
@label = label
|
|
122
|
+
@min_value = min_value
|
|
123
|
+
@max_value = max_value
|
|
124
|
+
@format = format
|
|
125
|
+
|
|
126
|
+
# Initialize value in session
|
|
127
|
+
initial_value = value || Date.today
|
|
128
|
+
St.set(@key, initial_value) if St.get(@key).nil?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def value
|
|
132
|
+
stored_value = St.get(@key)
|
|
133
|
+
return Date.today if stored_value.nil?
|
|
134
|
+
|
|
135
|
+
# Convert ISO8601 string back to Date if needed
|
|
136
|
+
stored_value.is_a?(String) ? Date.parse(stored_value) : stored_value
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def to_h
|
|
140
|
+
current_value = value
|
|
141
|
+
super.merge({
|
|
142
|
+
label: @label,
|
|
143
|
+
value: current_value.is_a?(Date) ? current_value.iso8601 : current_value,
|
|
144
|
+
min_value: @min_value&.is_a?(Date) ? @min_value.iso8601 : @min_value,
|
|
145
|
+
max_value: @max_value&.is_a?(Date) ? @max_value.iso8601 : @max_value,
|
|
146
|
+
format: @format
|
|
147
|
+
})
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class FileUploader < BaseComponent
|
|
152
|
+
attr_reader :label, :type, :accept_multiple_files, :max_size, :wide
|
|
153
|
+
|
|
154
|
+
DEFAULT_MAX_SIZE = 200 * 1024 * 1024 # 200 MB
|
|
155
|
+
|
|
156
|
+
def initialize(label, type: nil, accept_multiple_files: false, max_size: DEFAULT_MAX_SIZE, key: nil, help: nil, wide: false)
|
|
157
|
+
super(key: key, help: help)
|
|
158
|
+
@label = label
|
|
159
|
+
@type = type
|
|
160
|
+
@accept_multiple_files = accept_multiple_files
|
|
161
|
+
@max_size = max_size
|
|
162
|
+
@key ||= "file_uploader_#{label.downcase.gsub(/\W+/, '_')}"
|
|
163
|
+
@wide = wide
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def value
|
|
167
|
+
file_data = St.get(@key)
|
|
168
|
+
return nil if file_data.nil?
|
|
169
|
+
|
|
170
|
+
size = file_data['size']
|
|
171
|
+
name = file_data['name']
|
|
172
|
+
content_type = file_data['type']
|
|
173
|
+
file_path = file_data['path']
|
|
174
|
+
|
|
175
|
+
return nil if size > @max_size
|
|
176
|
+
|
|
177
|
+
# Create a file struct that reads from the server-side file path
|
|
178
|
+
OpenStruct.new(
|
|
179
|
+
name: name,
|
|
180
|
+
size: size,
|
|
181
|
+
content_type: content_type,
|
|
182
|
+
path: file_path,
|
|
183
|
+
read: -> {
|
|
184
|
+
# Read the file directly from the server's filesystem
|
|
185
|
+
# The path is now a real file path on the server
|
|
186
|
+
File.binread(file_path)
|
|
187
|
+
},
|
|
188
|
+
temp_path: file_path
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def self.finalizer(path)
|
|
193
|
+
proc { File.delete(path) if File.exist?(path) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Convenience method to read content regardless of storage type
|
|
197
|
+
def read
|
|
198
|
+
uploaded_file = value
|
|
199
|
+
uploaded_file&.read&.call # Call the read lambda on the OpenStruct
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Convenience method to read content as a Base64 encoded string
|
|
203
|
+
# Accepts the result of self.value as an argument to avoid re-fetching from session
|
|
204
|
+
def read_as_base64(uploaded_file_details = nil)
|
|
205
|
+
# If no details passed, try to get them via self.value (maintaining old behavior if called directly)
|
|
206
|
+
# However, the preferred way will be to pass the result of self.value
|
|
207
|
+
current_uploaded_file = uploaded_file_details || value
|
|
208
|
+
return nil unless current_uploaded_file && current_uploaded_file.path && current_uploaded_file.content_type
|
|
209
|
+
|
|
210
|
+
# Ensure the temporary file exists before trying to read it
|
|
211
|
+
return nil unless File.exist?(current_uploaded_file.path)
|
|
212
|
+
|
|
213
|
+
binary_data = File.binread(current_uploaded_file.path)
|
|
214
|
+
"data:#{current_uploaded_file.content_type};base64,#{Base64.strict_encode64(binary_data)}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def to_h
|
|
218
|
+
mime_types = if @type.is_a?(Array)
|
|
219
|
+
@type
|
|
220
|
+
elsif @type.is_a?(String)
|
|
221
|
+
[@type]
|
|
222
|
+
elsif @type.nil?
|
|
223
|
+
[]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
super.merge({
|
|
227
|
+
label: @label,
|
|
228
|
+
accept: mime_types,
|
|
229
|
+
multiple: @accept_multiple_files,
|
|
230
|
+
max_size: @max_size,
|
|
231
|
+
value: St.get(@key),
|
|
232
|
+
wide: @wide
|
|
233
|
+
})
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
class Select < BaseComponent
|
|
238
|
+
attr_reader :label, :options
|
|
239
|
+
|
|
240
|
+
def initialize(label, options, value: nil, key: nil, help: nil)
|
|
241
|
+
super(key: key, help: help)
|
|
242
|
+
@label = label
|
|
243
|
+
@options = map_options(options)
|
|
244
|
+
# Store initial value in session if key is provided and no value exists
|
|
245
|
+
if @key && St.get(@key).nil?
|
|
246
|
+
St.set(@key, value)
|
|
247
|
+
end
|
|
248
|
+
# Ensure @value is initialized even if not using session or no initial value in session
|
|
249
|
+
@value = St.get(@key) || value
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def value
|
|
253
|
+
# Retrieve value from session if key is provided
|
|
254
|
+
@key ? St.get(@key) : @value
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def map_options(options)
|
|
258
|
+
# Convert options to a hash format if they are not already
|
|
259
|
+
# This assumes options are either strings or hashes
|
|
260
|
+
if !options.is_a?(Array)
|
|
261
|
+
@errors << "Options must be an array"
|
|
262
|
+
end
|
|
263
|
+
options.map do |option|
|
|
264
|
+
if option.is_a?(Hash)
|
|
265
|
+
option.transform_keys(&:to_s)
|
|
266
|
+
else
|
|
267
|
+
{ value: option, label: option }
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def to_h
|
|
274
|
+
super.merge({
|
|
275
|
+
label: @label,
|
|
276
|
+
options: @options,
|
|
277
|
+
value: value
|
|
278
|
+
})
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
class Multiselect < BaseComponent
|
|
283
|
+
attr_reader :label, :options, :value
|
|
284
|
+
|
|
285
|
+
def initialize(label, options, default: [], key: nil, help: nil)
|
|
286
|
+
super(key: key, help: help)
|
|
287
|
+
@label = label
|
|
288
|
+
@options = options
|
|
289
|
+
@value = default & options # Intersection to ensure only valid options
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def to_h
|
|
293
|
+
super.merge({
|
|
294
|
+
label: @label,
|
|
295
|
+
options: @options,
|
|
296
|
+
value: @value
|
|
297
|
+
})
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
class Slider < BaseComponent
|
|
302
|
+
attr_reader :label, :min_value, :max_value, :value, :step
|
|
303
|
+
|
|
304
|
+
def initialize(label, min_value: 0, max_value: 100, value: nil, step: 1, key: nil, help: nil)
|
|
305
|
+
super(key: key, help: help)
|
|
306
|
+
@label = label
|
|
307
|
+
@min_value = min_value
|
|
308
|
+
@max_value = max_value
|
|
309
|
+
@value = value || min_value
|
|
310
|
+
@step = step
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def to_h
|
|
314
|
+
super.merge({
|
|
315
|
+
label: @label,
|
|
316
|
+
min_value: @min_value,
|
|
317
|
+
max_value: @max_value,
|
|
318
|
+
value: @value,
|
|
319
|
+
step: @step
|
|
320
|
+
})
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
class Radio < BaseComponent
|
|
325
|
+
attr_reader :label, :options, :index, :value
|
|
326
|
+
|
|
327
|
+
def initialize(label, options, index: 0, key: nil, help: nil)
|
|
328
|
+
super(key: key, help: help)
|
|
329
|
+
@label = label
|
|
330
|
+
@options = options
|
|
331
|
+
@index = [index, options.length - 1].min
|
|
332
|
+
@value = options[@index] if options.any?
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def to_h
|
|
336
|
+
super.merge({
|
|
337
|
+
label: @label,
|
|
338
|
+
options: @options,
|
|
339
|
+
value: @value
|
|
340
|
+
})
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
module St
|
|
346
|
+
def self.button(label, key: nil, help: nil)
|
|
347
|
+
Showtime::session.add_element(Showtime::Components::Button.new(label, key: key, help: help))
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def self.checkbox(label, value: false, key: nil, help: nil)
|
|
351
|
+
Showtime::session.add_element(Showtime::Components::Checkbox.new(label, value: value, key: key, help: help))
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def self.text_input(label, value: "", key: nil, help: nil)
|
|
355
|
+
Showtime::session.add_element(Showtime::Components::TextInput.new(label, value: value, key: key, help: help))
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def self.number_input(label, min_value: nil, max_value: nil, value: nil, step: 1, key: nil, help: nil)
|
|
359
|
+
Showtime::session.add_element(Showtime::Components::NumberInput.new(label, min_value: min_value, max_value: max_value, value: value, step: step, key: key, help: help))
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def self.text_area(label, value: "", height: nil, key: nil, help: nil, **kwargs)
|
|
363
|
+
Showtime::session.add_element(Showtime::Components::TextArea.new(label, value: value, height: height, key: key, help: help, **kwargs))
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def self.date_input(label, value: nil, min_value: nil, max_value: nil, format: 'YYYY-MM-DD', key: nil, help: nil)
|
|
367
|
+
Showtime::session.add_element(Showtime::Components::DateInput.new(label, value: value, min_value: min_value, max_value: max_value, format: format, key: key, help: help))
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def self.file_uploader(label, type: nil, accept_multiple_files: false, max_size: Showtime::Components::FileUploader::DEFAULT_MAX_SIZE, key: nil, help: nil, wide: false)
|
|
371
|
+
Showtime::session.add_element(Showtime::Components::FileUploader.new(label, type: type, accept_multiple_files: accept_multiple_files, max_size: max_size, key: key, help: help, wide: wide))
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def self.select(label, options, value: nil, key: nil, help: nil)
|
|
375
|
+
Showtime::session.add_element(Showtime::Components::Select.new(label, options, value: value, key: key, help: help))
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def self.multiselect(label, options, default: [], key: nil, help: nil)
|
|
379
|
+
Showtime::session.add_element(Showtime::Components::Multiselect.new(label, options, default: default, key: key, help: help))
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def self.slider(label, min_value: 0, max_value: 100, value: nil, step: 1, key: nil, help: nil)
|
|
383
|
+
Showtime::session.add_element(Showtime::Components::Slider.new(label, min_value: min_value, max_value: max_value, value: value, step: step, key: key, help: help))
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def self.radio(label, options, index: 0, key: nil, help: nil)
|
|
387
|
+
Showtime::session.add_element(Showtime::Components::Radio.new(label, options, index: index, key: key, help: help))
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module Showtime
|
|
4
|
+
module Components
|
|
5
|
+
class ContainerComponent < BaseComponent
|
|
6
|
+
attr_reader :children
|
|
7
|
+
|
|
8
|
+
def initialize(key: nil, help: nil)
|
|
9
|
+
super(key: key, help: help)
|
|
10
|
+
@children = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
super.merge({
|
|
15
|
+
children: @children.map(&:to_h)
|
|
16
|
+
})
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Container < ContainerComponent
|
|
21
|
+
attr_reader :title, :border, :grow
|
|
22
|
+
|
|
23
|
+
def initialize(title: nil, border: true, grow: false, key: nil, help: nil)
|
|
24
|
+
super(key: key, help: help)
|
|
25
|
+
@title = title
|
|
26
|
+
@border = border
|
|
27
|
+
@grow = grow
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
super.merge({
|
|
32
|
+
title: @title,
|
|
33
|
+
border: @border,
|
|
34
|
+
grow: @grow
|
|
35
|
+
})
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class SplitLayout < ContainerComponent
|
|
40
|
+
attr_reader :direction, :sizes, :height
|
|
41
|
+
|
|
42
|
+
def initialize(direction: 'horizontal', sizes: nil, height: nil, ttype: 'card', key: nil, help: nil)
|
|
43
|
+
super(key: key, help: help)
|
|
44
|
+
@direction = direction
|
|
45
|
+
@sizes = sizes
|
|
46
|
+
@height = height
|
|
47
|
+
@ttype = ttype
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h
|
|
51
|
+
super.merge({
|
|
52
|
+
direction: @direction,
|
|
53
|
+
sizes: @sizes,
|
|
54
|
+
height: @height,
|
|
55
|
+
ttype: @ttype
|
|
56
|
+
})
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class Collapse < ContainerComponent
|
|
61
|
+
attr_reader :label, :expanded
|
|
62
|
+
|
|
63
|
+
def initialize(label, expanded: false, key: nil, help: nil)
|
|
64
|
+
super(key: key, help: help)
|
|
65
|
+
@label = label
|
|
66
|
+
@expanded = expanded
|
|
67
|
+
St.set(@key, expanded) if St.get(@key).nil?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def value
|
|
71
|
+
St.get(@key)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_h
|
|
75
|
+
super.merge({
|
|
76
|
+
label: @label,
|
|
77
|
+
expanded: value
|
|
78
|
+
})
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
module St
|
|
84
|
+
def self.container(title: nil, border: true, grow: false, key: nil, help: nil, &block)
|
|
85
|
+
container = Showtime::Components::Container.new(title: title, border: border, grow: grow, key: key, help: help)
|
|
86
|
+
element = Showtime::session.add_element(container)
|
|
87
|
+
|
|
88
|
+
if block_given?
|
|
89
|
+
Showtime::session.with_container(container) do
|
|
90
|
+
yield
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
element
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Build a responsive split layout container.
|
|
98
|
+
#
|
|
99
|
+
# @param direction [String]
|
|
100
|
+
# 'horizontal' (default) or 'vertical'
|
|
101
|
+
# @param sizes [Array<Integer>, nil]
|
|
102
|
+
# column sizes for each child. When provided in horizontal mode,
|
|
103
|
+
# they are interpreted on a 24-column grid (Ant Design) and should sum to 24
|
|
104
|
+
# so sibling panels align; when omitted, equal distribution across 24 columns is used.
|
|
105
|
+
# @param height [Integer, String, nil] optional height for vertical layouts (number => px, string => CSS value)
|
|
106
|
+
# @param type [String] visual style for the container (card/ghost/success/info/warning/error)
|
|
107
|
+
# @param key [String, nil] component key
|
|
108
|
+
# @param help [String, nil] optional tooltip/help text
|
|
109
|
+
def self.split_layout(direction: 'horizontal', sizes: nil, height: nil, type: 'card', key: nil, help: nil, &block)
|
|
110
|
+
container = Showtime::Components::SplitLayout.new(direction: direction, sizes: sizes, height: height, ttype: type, key: key, help: help)
|
|
111
|
+
element = Showtime::session.add_element(container)
|
|
112
|
+
|
|
113
|
+
if block_given?
|
|
114
|
+
Showtime::session.with_container(container) do
|
|
115
|
+
yield
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
element
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.collapse(label, expanded: false, key: nil, help: nil, &block)
|
|
123
|
+
container = Showtime::Components::Collapse.new(label, expanded: expanded, key: key, help: help)
|
|
124
|
+
element = Showtime::session.add_element(container)
|
|
125
|
+
|
|
126
|
+
if block_given?
|
|
127
|
+
Showtime::session.with_container(container) do
|
|
128
|
+
yield
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
element
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
require 'base64'
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../helpers'
|
|
5
|
+
|
|
6
|
+
module Showtime
|
|
7
|
+
module Components
|
|
8
|
+
class Image < BaseComponent
|
|
9
|
+
attr_reader :src, :caption, :width, :use_column_width
|
|
10
|
+
|
|
11
|
+
def initialize(src, caption: nil, width: nil, use_column_width: true, key: nil, help: nil)
|
|
12
|
+
super(key: key, help: help)
|
|
13
|
+
@src = resolve_src(src)
|
|
14
|
+
@caption = caption
|
|
15
|
+
@width = width
|
|
16
|
+
@use_column_width = use_column_width
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve_src(src)
|
|
20
|
+
# Only process if it's a string, not a URL, not a data URI, and not already absolute
|
|
21
|
+
if src.is_a?(String) &&
|
|
22
|
+
!src.start_with?('http://', 'https://') &&
|
|
23
|
+
!src.include?(';base64,') &&
|
|
24
|
+
!(defined?(Pathname) && Pathname.new(src).absolute?) # Check Pathname.new(src).absolute? only if Pathname is defined
|
|
25
|
+
# The '1' offset is because St.image calls Image.new, which then calls resolve_src.
|
|
26
|
+
# We want the path relative to the script calling St.image.
|
|
27
|
+
return Showtime::Helpers.absolute_path(src)
|
|
28
|
+
end
|
|
29
|
+
src # Return original src if it's a URL, data URI, absolute, or not a string
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
Showtime::Logger.debug("Image src: #{@src}")
|
|
34
|
+
image_data = if @src.is_a?(String) && File.exist?(@src)
|
|
35
|
+
# It's a file path
|
|
36
|
+
mime_type = case File.extname(@src).downcase
|
|
37
|
+
when '.png' then 'image/png'
|
|
38
|
+
when '.jpg', '.jpeg' then 'image/jpeg'
|
|
39
|
+
when '.gif' then 'image/gif'
|
|
40
|
+
when '.svg' then 'image/svg+xml'
|
|
41
|
+
else 'application/octet-stream'
|
|
42
|
+
end
|
|
43
|
+
"data:#{mime_type};base64,#{Base64.strict_encode64(File.read(@src))}"
|
|
44
|
+
elsif @src.is_a?(String) && @src.start_with?('http')
|
|
45
|
+
# It's a URL
|
|
46
|
+
@src
|
|
47
|
+
elsif @src.is_a?(String) && @src.include?(';base64,')
|
|
48
|
+
# It's already a data URI
|
|
49
|
+
@src
|
|
50
|
+
else
|
|
51
|
+
# Assume it's binary data (or needs default handling if not a recognized string type)
|
|
52
|
+
# This branch might need further refinement if @src can be non-string binary
|
|
53
|
+
Showtime::Logger.warn("Image src is not a file path, URL, or data URI. Attempting to treat as binary. Src: #{@src.class}")
|
|
54
|
+
"data:image/png;base64,#{Base64.strict_encode64(@src.to_s)}" # Ensure @src is stringified if it's raw binary
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
super.merge({
|
|
58
|
+
src: image_data,
|
|
59
|
+
caption: @caption,
|
|
60
|
+
width: @width,
|
|
61
|
+
use_column_width: @use_column_width
|
|
62
|
+
})
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
module St
|
|
68
|
+
def self.image(src, caption: nil, width: nil, use_column_width: false, key: nil, help: nil)
|
|
69
|
+
component = Components::Image.new(src, caption: caption, width: width, use_column_width: use_column_width, key: key, help: help)
|
|
70
|
+
Showtime.session.add_element(component)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|