ruflet 0.0.4 → 0.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01d8a3779e874187cf24c4373efa47dbb8541c9495e8b54534d1c4028bf0b90c
4
- data.tar.gz: 81d1d6959b1d6e144f24e3c6586f35b6c32fc5f971f039b702a607fe3ef1ea15
3
+ metadata.gz: 8f114a59fe4013e5313b51fc7b8b3373dfa7120e57422bc33abb2e224f9724d6
4
+ data.tar.gz: fe0b491c3eff667a72c31521209953a2f7ba8249c1d16f6ec9ac08243099f850
5
5
  SHA512:
6
- metadata.gz: 303761d68b2adda5d36edf77e6270c6668ca166defbc71035df969e012c7539bb6c39b9cd69a20df3ab2c849f1fa20cdb857b663db59eb274bcb36cae206661e
7
- data.tar.gz: b0892bfbb584db2187782958b4d67c86ffbf2ed2899974389ac79b89d770d9898f099eef8ec42477d19b8104a4b5496ed7655b2882f4b7e149cd7d5f0a4b2428
6
+ metadata.gz: cbf26f1c4752d3fdd13600ff8b0bd305e3e669165c9cda9d094d4e4687f801adcb1df015230a509c016f8305f195c32459af74eeacdc8829475f62ac8c7af591
7
+ data.tar.gz: a59f249b3b71255a4b499e8e87c69216c3e61d50c974a786cc1d926b7d502b500effe4161b5c2d8c3c384078ead1cfe1194106c872ca4a17bdafb05f180e36f7
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.4" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.5" unless const_defined?(:VERSION)
5
5
  end
@@ -5,6 +5,15 @@ require_relative "ui/control_factory"
5
5
 
6
6
  module Ruflet
7
7
  module DSL
8
+ DURATION_FACTORS_MS = {
9
+ days: 86_400_000.0,
10
+ hours: 3_600_000.0,
11
+ minutes: 60_000.0,
12
+ seconds: 1_000.0,
13
+ milliseconds: 1.0,
14
+ microseconds: 0.001
15
+ }.freeze
16
+
8
17
  module_function
9
18
 
10
19
  def _pending_app
@@ -95,6 +104,7 @@ module Ruflet
95
104
  def cupertinodialogaction(**props) = _pending_app.cupertinodialogaction(**props)
96
105
  def cupertino_navigation_bar(**props) = _pending_app.cupertino_navigation_bar(**props)
97
106
  def cupertinonavigationbar(**props) = _pending_app.cupertinonavigationbar(**props)
107
+ def duration(**parts) = duration_in_milliseconds(parts)
98
108
 
99
109
  class App
100
110
  include UI::ControlMethods
@@ -146,6 +156,10 @@ module Ruflet
146
156
  c
147
157
  end
148
158
 
159
+ def duration(**parts)
160
+ DSL.duration(**parts)
161
+ end
162
+
149
163
  def run
150
164
  app_roots = @roots
151
165
  page_props = @page_props.dup
@@ -177,6 +191,27 @@ module Ruflet
177
191
  "#{type}_#{@seq}"
178
192
  end
179
193
 
194
+ def duration_in_milliseconds(parts)
195
+ DSL.send(:duration_in_milliseconds, parts)
196
+ end
197
+
198
+ end
199
+
200
+ def duration_in_milliseconds(parts)
201
+ return 0 if parts.nil? || parts.empty?
202
+
203
+ DURATION_FACTORS_MS.reduce(0.0) do |sum, (key, factor)|
204
+ sum + read_duration_part(parts, key) * factor
205
+ end.round
206
+ end
207
+
208
+ def read_duration_part(parts, key)
209
+ raw = parts[key] || parts[key.to_s]
210
+ return 0.0 if raw.nil?
211
+ return raw.to_f if raw.is_a?(Numeric)
212
+ return raw.to_f if raw.is_a?(String) && raw.match?(/\A-?\d+(\.\d+)?\z/)
213
+
214
+ 0.0
180
215
  end
181
216
  end
182
217
  end
@@ -3,49 +3,29 @@
3
3
  require_relative "event"
4
4
  require "ruflet_protocol"
5
5
  require_relative "control"
6
- require_relative "ui/control_methods"
7
6
  require_relative "ui/widget_builder"
8
7
  require_relative "icons/material_icon_lookup"
9
8
  require_relative "icons/cupertino_icon_lookup"
10
- begin
11
- require "set"
12
- rescue LoadError
13
- class Set
14
- def initialize
15
- @index = {}
16
- end
17
-
18
- def include?(value)
19
- @index.key?(value)
20
- end
21
-
22
- def <<(value)
23
- @index[value] = true
24
- self
25
- end
26
- end
27
- end
28
-
29
- begin
30
- require "cgi"
31
- rescue LoadError
32
- module CGI
33
- module_function
34
-
35
- def escape(text)
36
- value = text.to_s
37
- value.gsub(/[^a-zA-Z0-9_.~-]/) { |ch| "%%%02X" % ch.ord }
38
- end
39
- end
40
- end
9
+ require "set"
10
+ require "cgi"
41
11
 
42
12
  module Ruflet
43
13
  class Page
44
- include UI::ControlMethods
45
-
46
14
  PAGE_PROP_KEYS = %w[route title vertical_alignment horizontal_alignment scroll].freeze
47
15
  DIALOG_PROP_KEYS = %w[dialog snack_bar bottom_sheet].freeze
48
16
  BUTTON_TEXT_TYPES = %w[button elevatedbutton textbutton filledbutton].freeze
17
+ DEPRECATED_PAGE_WIDGET_METHODS = %i[
18
+ control widget view column center row stack container gesture_detector gesturedetector draggable
19
+ drag_target dragtarget text button elevated_button elevatedbutton text_button textbutton filled_button
20
+ filledbutton icon_button iconbutton text_field textfield checkbox radio radio_group radiogroup
21
+ alert_dialog alertdialog markdown icon image app_bar appbar floating_action_button snack_bar snackbar
22
+ bottom_sheet bottomsheet tabs tab tab_bar tabbar tab_bar_view tabbarview navigation_bar navigationbar
23
+ navigation_bar_destination navigationbardestination fab cupertino_button
24
+ cupertinobutton cupertino_filled_button cupertinofilledbutton cupertino_text_field cupertinotextfield
25
+ cupertino_switch cupertinoswitch cupertino_slider cupertinoslider cupertino_alert_dialog
26
+ cupertinoalertdialog cupertino_action_sheet cupertinoactionsheet cupertino_dialog_action
27
+ cupertinodialogaction cupertino_navigation_bar cupertinonavigationbar
28
+ ].freeze
49
29
 
50
30
  attr_reader :session_id, :client_details, :views
51
31
 
@@ -104,16 +84,6 @@ module Ruflet
104
84
  @page_props["route"] = value
105
85
  end
106
86
 
107
- def scroll
108
- @page_props["scroll"]
109
- end
110
-
111
- def scroll=(value)
112
- v = normalize_value("scroll", value)
113
- @page_props["scroll"] = v
114
- @view_props["scroll"] = v
115
- end
116
-
117
87
  def vertical_alignment
118
88
  @page_props["vertical_alignment"] || @view_props["vertical_alignment"]
119
89
  end
@@ -163,6 +133,11 @@ module Ruflet
163
133
  self
164
134
  end
165
135
 
136
+ def views=(value)
137
+ @views = Array(value).compact
138
+ self
139
+ end
140
+
166
141
  def services
167
142
  @services_container.props["_services"] ||= []
168
143
  end
@@ -181,11 +156,6 @@ module Ruflet
181
156
  self
182
157
  end
183
158
 
184
- def views=(value)
185
- @views = Array(value).compact
186
- self
187
- end
188
-
189
159
  def go(route, **query_params)
190
160
  @page_props["route"] = build_route(route, query_params)
191
161
  dispatch_page_event(name: "route_change", data: @page_props["route"])
@@ -212,22 +182,10 @@ module Ruflet
212
182
  add(*builder.children)
213
183
  end
214
184
 
215
- def appbar(**props, &block)
216
- return @view_props["appbar"] if props.empty? && !block
217
-
218
- WidgetBuilder.new.appbar(**props, &block)
219
- end
220
-
221
185
  def appbar=(value)
222
186
  @view_props["appbar"] = value
223
187
  end
224
188
 
225
- def floating_action_button(**props, &block)
226
- return @view_props["floating_action_button"] if props.empty? && !block
227
-
228
- WidgetBuilder.new.floating_action_button(**props, &block)
229
- end
230
-
231
189
  def floating_action_button=(value)
232
190
  @view_props["floating_action_button"] = value
233
191
  end
@@ -239,40 +197,20 @@ module Ruflet
239
197
  refresh_dialogs_container!
240
198
  end
241
199
 
242
- def snack_bar(**props, &block)
243
- return @snack_bar if props.empty? && !block
244
-
245
- super
246
- end
247
-
248
200
  def snack_bar=(value)
249
201
  @snack_bar = value
250
202
  refresh_dialogs_container!
251
203
  end
252
204
 
253
- def snackbar(**props, &block)
254
- snack_bar(**props, &block)
255
- end
256
-
257
205
  def snackbar=(value)
258
206
  self.snack_bar = value
259
207
  end
260
208
 
261
- def bottom_sheet(**props, &block)
262
- return @bottom_sheet if props.empty? && !block
263
-
264
- super
265
- end
266
-
267
209
  def bottom_sheet=(value)
268
210
  @bottom_sheet = value
269
211
  refresh_dialogs_container!
270
212
  end
271
213
 
272
- def bottomsheet(**props, &block)
273
- bottom_sheet(**props, &block)
274
- end
275
-
276
214
  def bottomsheet=(value)
277
215
  self.bottom_sheet = value
278
216
  end
@@ -290,12 +228,48 @@ module Ruflet
290
228
  self
291
229
  end
292
230
 
231
+ def invoke(control_or_id, method_name, args: nil, timeout: 10)
232
+ control = resolve_control(control_or_id)
233
+ return nil unless control
234
+
235
+ call_id = "call_#{Ruflet::Control.generate_id}"
236
+ send_message(Protocol::ACTIONS[:invoke_control_method], {
237
+ "control_id" => control.wire_id,
238
+ "call_id" => call_id,
239
+ "name" => method_name.to_s,
240
+ "args" => args,
241
+ "timeout" => timeout
242
+ })
243
+
244
+ call_id
245
+ end
246
+
247
+ def launch_url(url, mode: "external_application", web_view_configuration: nil, browser_configuration: nil, web_only_window_name: nil, timeout: 10)
248
+ launcher = ensure_url_launcher_service
249
+ invoke(
250
+ launcher,
251
+ "launch_url",
252
+ args: {
253
+ "url" => url,
254
+ "mode" => mode,
255
+ "web_view_configuration" => web_view_configuration,
256
+ "browser_configuration" => browser_configuration,
257
+ "web_only_window_name" => web_only_window_name
258
+ }.compact,
259
+ timeout: timeout
260
+ )
261
+ end
262
+
263
+ def can_launch_url(url, timeout: 10)
264
+ launcher = ensure_url_launcher_service
265
+ invoke(launcher, "can_launch_url", args: { "url" => url }, timeout: timeout)
266
+ end
267
+
293
268
  def pop_dialog
294
269
  dialog_control = latest_open_dialog
295
270
  return nil unless dialog_control
296
271
 
297
272
  dialog_control.props["open"] = false
298
- @dialogs.delete(dialog_control)
299
273
  refresh_dialogs_container!
300
274
  push_dialogs_update!
301
275
  dialog_control
@@ -316,21 +290,7 @@ module Ruflet
316
290
  control = resolve_control(control_or_id)
317
291
  return self unless control
318
292
 
319
- visited = Set.new
320
- props.each_value { |value| register_embedded_value(value, visited) }
321
-
322
- raw_props = props.dup
323
- if BUTTON_TEXT_TYPES.include?(control.type)
324
- if raw_props.key?(:text) || raw_props.key?("text")
325
- text_value = raw_props.key?(:text) ? raw_props.delete(:text) : raw_props.delete("text")
326
- raw_props[:content] = text_value unless raw_props.key?(:content) || raw_props.key?("content")
327
- end
328
- end
329
-
330
- normalized_control_props = control.send(:normalize_props, raw_props)
331
- normalized_control_props.each { |k, v| control.props[k] = v }
332
-
333
- patch = normalize_props(raw_props)
293
+ patch = normalize_props(props)
334
294
  if BUTTON_TEXT_TYPES.include?(control.type) && patch.key?("text")
335
295
  patch["content"] = patch.delete("text")
336
296
  end
@@ -345,22 +305,6 @@ module Ruflet
345
305
  self
346
306
  end
347
307
 
348
- def invoke(control_or_id, method_name, args: nil, timeout: 10)
349
- control = resolve_control(control_or_id)
350
- return nil unless control
351
-
352
- call_id = "call_#{Ruflet::Control.generate_id}"
353
- send_message(Protocol::ACTIONS[:invoke_control_method], {
354
- "control_id" => control.wire_id,
355
- "call_id" => call_id,
356
- "name" => method_name.to_s,
357
- "args" => args,
358
- "timeout" => timeout
359
- })
360
-
361
- call_id
362
- end
363
-
364
308
  def patch_page(control_id, **props)
365
309
  update(control_id, **props)
366
310
  end
@@ -398,6 +342,45 @@ module Ruflet
398
342
  end
399
343
  end
400
344
 
345
+ def method_missing(name, *args, &block)
346
+ method_name = name.to_s
347
+ prop_name = method_name.delete_suffix("=")
348
+
349
+ if method_name.end_with?("=")
350
+ if DEPRECATED_PAGE_WIDGET_METHODS.include?(prop_name.to_sym)
351
+ Kernel.warn("[DEPRECATION] `page.#{prop_name}(...)` is no longer supported.")
352
+ raise NoMethodError, "Use `#{prop_name}(...)` as a free widget helper, then attach with `page.add(...)`."
353
+ end
354
+ assign_split_prop(prop_name, normalize_value(prop_name, args.first))
355
+ return args.first
356
+ end
357
+
358
+ if args.empty? && !block
359
+ return @page_props[method_name] if @page_props.key?(method_name)
360
+ return @view_props[method_name] if @view_props.key?(method_name)
361
+ return instance_variable_get("@#{method_name}") if DIALOG_PROP_KEYS.include?(method_name)
362
+ end
363
+
364
+ if DEPRECATED_PAGE_WIDGET_METHODS.include?(name.to_sym)
365
+ Kernel.warn("[DEPRECATION] `page.#{name}(...)` is no longer supported.")
366
+ raise NoMethodError, "Use `#{name}(...)` as a free widget helper, then attach with `page.add(...)`."
367
+ end
368
+
369
+ super
370
+ end
371
+
372
+ def respond_to_missing?(name, include_private = false)
373
+ method_name = name.to_s
374
+ prop_name = method_name.delete_suffix("=")
375
+ DEPRECATED_PAGE_WIDGET_METHODS.include?(name.to_sym) ||
376
+ DEPRECATED_PAGE_WIDGET_METHODS.include?(prop_name.to_sym) ||
377
+ method_name.end_with?("=") ||
378
+ @page_props.key?(method_name) ||
379
+ @view_props.key?(method_name) ||
380
+ DIALOG_PROP_KEYS.include?(method_name) ||
381
+ super
382
+ end
383
+
401
384
  private
402
385
 
403
386
  def build_widget(type, **props, &block) = WidgetBuilder.new.control(type, **props, &block)
@@ -489,7 +472,7 @@ module Ruflet
489
472
  def resolve_control(control_or_id)
490
473
  if control_or_id.respond_to?(:wire_id)
491
474
  control_or_id
492
- elsif numeric_string?(control_or_id.to_s)
475
+ elsif control_or_id.to_s.match?(/^\d+$/)
493
476
  @wire_index[control_or_id.to_i]
494
477
  else
495
478
  @control_index[control_or_id.to_s]
@@ -509,32 +492,10 @@ module Ruflet
509
492
  return codepoint unless codepoint.nil?
510
493
  end
511
494
 
512
- if value.is_a?(Ruflet::Control)
513
- register_control_tree(value, Set.new)
514
- return value.to_patch
515
- end
516
- return serialize_value(value) if value.is_a?(Array) || value.is_a?(Hash)
517
-
518
495
  return value.value if value.is_a?(Ruflet::IconData)
519
496
  value.is_a?(Symbol) ? value.to_s : value
520
497
  end
521
498
 
522
- def serialize_value(value)
523
- case value
524
- when Ruflet::Control
525
- register_control_tree(value, Set.new)
526
- value.to_patch
527
- when Ruflet::IconData
528
- value.value
529
- when Array
530
- value.map { |v| serialize_value(v) }
531
- when Hash
532
- value.transform_values { |v| serialize_value(v) }
533
- else
534
- value
535
- end
536
- end
537
-
538
499
  def build_route(route, query_params = {})
539
500
  base = route.to_s
540
501
  return base if query_params.nil? || query_params.empty?
@@ -586,12 +547,6 @@ module Ruflet
586
547
  key == "icon" || key.end_with?("_icon")
587
548
  end
588
549
 
589
- def numeric_string?(value)
590
- return false if value.empty?
591
- value.each_byte { |b| return false unless b >= 0x30 && b <= 0x39 }
592
- true
593
- end
594
-
595
550
  def refresh_dialogs_container!
596
551
  dialog_controls = (@dialogs + dialog_slots).uniq
597
552
  @dialogs_container.props["controls"] = dialog_controls
@@ -679,25 +634,20 @@ module Ruflet
679
634
  end
680
635
 
681
636
  def resolve_icon_codepoint(value)
682
- return nil unless value.is_a?(Integer) || value.is_a?(Symbol) || value.is_a?(String)
683
-
684
- codepoint = nil
685
- begin
686
- codepoint = Ruflet::MaterialIconLookup.codepoint_for(value)
687
- rescue NameError
688
- codepoint = nil
637
+ codepoint = Ruflet::MaterialIconLookup.codepoint_for(value)
638
+ if codepoint.nil? || codepoint == value
639
+ codepoint = Ruflet::CupertinoIconLookup.codepoint_for(value)
689
640
  end
641
+ codepoint
642
+ end
690
643
 
691
- if codepoint.nil? || (value.is_a?(Integer) && codepoint == value)
692
- begin
693
- cupertino = Ruflet::CupertinoIconLookup.codepoint_for(value)
694
- codepoint = cupertino unless cupertino.nil?
695
- rescue NameError
696
- codepoint = nil
697
- end
698
- end
644
+ def ensure_url_launcher_service
645
+ launcher = services.find { |service| service.is_a?(Control) && service.type == "url_launcher" }
646
+ return launcher if launcher
699
647
 
700
- codepoint
648
+ launcher = build_widget(:url_launcher)
649
+ add_service(launcher)
650
+ launcher
701
651
  end
702
652
  end
703
653
  end
@@ -7,6 +7,61 @@ module Ruflet
7
7
  def initialize(id: nil, **props)
8
8
  super(type: "snackbar", id: id, **props)
9
9
  end
10
+
11
+ private
12
+
13
+ def preprocess_props(props)
14
+ mapped = props.dup
15
+ key = if mapped.key?(:duration)
16
+ :duration
17
+ elsif mapped.key?("duration")
18
+ "duration"
19
+ end
20
+ return mapped unless key
21
+
22
+ mapped[key] = normalize_duration_ms(mapped[key])
23
+ mapped
24
+ end
25
+
26
+ def normalize_duration_ms(value)
27
+ return value.to_i if value.is_a?(Numeric)
28
+ return value.to_i if value.is_a?(String) && value.match?(/\A\d+(\.\d+)?\z/)
29
+
30
+ parts =
31
+ if value.is_a?(Hash)
32
+ value
33
+ elsif value.respond_to?(:to_h)
34
+ value.to_h
35
+ else
36
+ nil
37
+ end
38
+ return value unless parts.is_a?(Hash)
39
+
40
+ days = read_number(parts, "days")
41
+ hours = read_number(parts, "hours")
42
+ minutes = read_number(parts, "minutes")
43
+ seconds = read_number(parts, "seconds")
44
+ milliseconds = read_number(parts, "milliseconds")
45
+ microseconds = read_number(parts, "microseconds")
46
+
47
+ total_ms = 0.0
48
+ total_ms += days * 86_400_000.0
49
+ total_ms += hours * 3_600_000.0
50
+ total_ms += minutes * 60_000.0
51
+ total_ms += seconds * 1_000.0
52
+ total_ms += milliseconds
53
+ total_ms += microseconds / 1000.0
54
+ total_ms.round
55
+ end
56
+
57
+ def read_number(parts, key)
58
+ raw = parts[key] || parts[key.to_sym]
59
+ return 0.0 if raw.nil?
60
+ return raw.to_f if raw.is_a?(Numeric)
61
+ return raw.to_f if raw.is_a?(String) && raw.match?(/\A-?\d+(\.\d+)?\z/)
62
+
63
+ 0.0
64
+ end
10
65
  end
11
66
  end
12
67
  end
@@ -74,6 +74,7 @@ module Ruflet
74
74
  def cupertinodialogaction(**props) = control_delegate.cupertinodialogaction(**props)
75
75
  def cupertino_navigation_bar(**props) = control_delegate.cupertino_navigation_bar(**props)
76
76
  def cupertinonavigationbar(**props) = control_delegate.cupertinonavigationbar(**props)
77
+ def duration(**parts) = control_delegate.duration(**parts)
77
78
 
78
79
  private
79
80
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa