ruflet_core 0.0.12 → 0.0.14

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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ruflet/version.rb +1 -1
  3. data/lib/ruflet_protocol/ruflet/protocol.rb +50 -4
  4. data/lib/ruflet_ui/ruflet/control.rb +11 -4
  5. data/lib/ruflet_ui/ruflet/dsl.rb +229 -37
  6. data/lib/ruflet_ui/ruflet/events/gesture_events.rb +113 -9
  7. data/lib/ruflet_ui/ruflet/page.rb +884 -162
  8. data/lib/ruflet_ui/ruflet/types/animation.rb +110 -0
  9. data/lib/ruflet_ui/ruflet/ui/control_registry.rb +1 -0
  10. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactionsheet_control.rb +13 -0
  11. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactionsheetaction_control.rb +4 -0
  12. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactivityindicator_control.rb +4 -0
  13. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoalertdialog_control.rb +17 -0
  14. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoappbar_control.rb +3 -0
  15. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinobottomsheet_control.rb +4 -0
  16. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinobutton_control.rb +9 -0
  17. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocheckbox_control.rb +6 -0
  18. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocontextmenu_control.rb +11 -0
  19. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocontextmenuaction_control.rb +4 -0
  20. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinodatepicker_control.rb +14 -0
  21. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinodialogaction_control.rb +4 -0
  22. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinofilledbutton_control.rb +9 -0
  23. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinolisttile_control.rb +12 -0
  24. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinonavigationbar_control.rb +11 -0
  25. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinopicker_control.rb +18 -0
  26. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoradio_control.rb +6 -0
  27. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinosegmentedbutton_control.rb +2 -0
  28. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoslider_control.rb +7 -0
  29. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoslidingsegmentedbutton_control.rb +3 -0
  30. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoswitch_control.rb +7 -1
  31. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotextfield_control.rb +7 -0
  32. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotimerpicker_control.rb +18 -0
  33. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotintedbutton_control.rb +9 -0
  34. data/lib/ruflet_ui/ruflet/ui/controls/materials/alertdialog_control.rb +4 -0
  35. data/lib/ruflet_ui/ruflet/ui/controls/materials/appbar_control.rb +4 -0
  36. data/lib/ruflet_ui/ruflet/ui/controls/materials/audio_control.rb +33 -1
  37. data/lib/ruflet_ui/ruflet/ui/controls/materials/autocomplete_control.rb +4 -0
  38. data/lib/ruflet_ui/ruflet/ui/controls/materials/autocompletesuggestion_control.rb +26 -0
  39. data/lib/ruflet_ui/ruflet/ui/controls/materials/badge_control.rb +7 -0
  40. data/lib/ruflet_ui/ruflet/ui/controls/materials/banner_control.rb +11 -1
  41. data/lib/ruflet_ui/ruflet/ui/controls/materials/bottomappbar_control.rb +2 -0
  42. data/lib/ruflet_ui/ruflet/ui/controls/materials/bottomsheet_control.rb +5 -0
  43. data/lib/ruflet_ui/ruflet/ui/controls/materials/button_control.rb +2 -0
  44. data/lib/ruflet_ui/ruflet/ui/controls/materials/card_control.rb +4 -0
  45. data/lib/ruflet_ui/ruflet/ui/controls/materials/chip_control.rb +11 -0
  46. data/lib/ruflet_ui/ruflet/ui/controls/materials/circleavatar_control.rb +12 -0
  47. data/lib/ruflet_ui/ruflet/ui/controls/materials/contextmenu_control.rb +19 -0
  48. data/lib/ruflet_ui/ruflet/ui/controls/materials/datacell_control.rb +4 -0
  49. data/lib/ruflet_ui/ruflet/ui/controls/materials/datacolumn_control.rb +4 -0
  50. data/lib/ruflet_ui/ruflet/ui/controls/materials/datatable_control.rb +34 -0
  51. data/lib/ruflet_ui/ruflet/ui/controls/materials/datepicker_control.rb +18 -0
  52. data/lib/ruflet_ui/ruflet/ui/controls/materials/daterangepicker_control.rb +20 -0
  53. data/lib/ruflet_ui/ruflet/ui/controls/materials/divider_control.rb +9 -0
  54. data/lib/ruflet_ui/ruflet/ui/controls/materials/dropdown_control.rb +11 -0
  55. data/lib/ruflet_ui/ruflet/ui/controls/materials/dropdownoption_control.rb +5 -0
  56. data/lib/ruflet_ui/ruflet/ui/controls/materials/expansionpanel_control.rb +8 -0
  57. data/lib/ruflet_ui/ruflet/ui/controls/materials/expansionpanellist_control.rb +6 -0
  58. data/lib/ruflet_ui/ruflet/ui/controls/materials/expansiontile_control.rb +6 -0
  59. data/lib/ruflet_ui/ruflet/ui/controls/materials/filledbutton_control.rb +5 -0
  60. data/lib/ruflet_ui/ruflet/ui/controls/materials/fillediconbutton_control.rb +8 -0
  61. data/lib/ruflet_ui/ruflet/ui/controls/materials/filledtonalbutton_control.rb +5 -0
  62. data/lib/ruflet_ui/ruflet/ui/controls/materials/filledtonaliconbutton_control.rb +8 -0
  63. data/lib/ruflet_ui/ruflet/ui/controls/materials/floatingactionbutton_control.rb +20 -0
  64. data/lib/ruflet_ui/ruflet/ui/controls/materials/iconbutton_control.rb +4 -0
  65. data/lib/ruflet_ui/ruflet/ui/controls/materials/listtile_control.rb +4 -0
  66. data/lib/ruflet_ui/ruflet/ui/controls/materials/map_controls.rb +311 -0
  67. data/lib/ruflet_ui/ruflet/ui/controls/materials/menubar_control.rb +2 -0
  68. data/lib/ruflet_ui/ruflet/ui/controls/materials/menuitembutton_control.rb +6 -0
  69. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationbar_control.rb +11 -1
  70. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationbardestination_control.rb +2 -0
  71. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationdrawer_control.rb +12 -1
  72. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationdrawerdestination_control.rb +2 -0
  73. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationrail_control.rb +21 -0
  74. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationraildestination_control.rb +2 -0
  75. data/lib/ruflet_ui/ruflet/ui/controls/materials/outlinedbutton_control.rb +5 -0
  76. data/lib/ruflet_ui/ruflet/ui/controls/materials/outlinediconbutton_control.rb +8 -0
  77. data/lib/ruflet_ui/ruflet/ui/controls/materials/popupmenubutton_control.rb +4 -0
  78. data/lib/ruflet_ui/ruflet/ui/controls/materials/popupmenuitem_control.rb +6 -0
  79. data/lib/ruflet_ui/ruflet/ui/controls/materials/progressbar_control.rb +9 -0
  80. data/lib/ruflet_ui/ruflet/ui/controls/materials/progressring_control.rb +8 -0
  81. data/lib/ruflet_ui/ruflet/ui/controls/materials/rangeslider_control.rb +13 -0
  82. data/lib/ruflet_ui/ruflet/ui/controls/materials/ruflet_controls.rb +24 -0
  83. data/lib/ruflet_ui/ruflet/ui/controls/materials/searchbar_control.rb +28 -0
  84. data/lib/ruflet_ui/ruflet/ui/controls/materials/segment_control.rb +5 -0
  85. data/lib/ruflet_ui/ruflet/ui/controls/materials/segmentedbutton_control.rb +19 -0
  86. data/lib/ruflet_ui/ruflet/ui/controls/materials/selectionarea_control.rb +4 -0
  87. data/lib/ruflet_ui/ruflet/ui/controls/materials/snackbar_control.rb +5 -0
  88. data/lib/ruflet_ui/ruflet/ui/controls/materials/submenubutton_control.rb +8 -0
  89. data/lib/ruflet_ui/ruflet/ui/controls/materials/tab_control.rb +7 -0
  90. data/lib/ruflet_ui/ruflet/ui/controls/materials/tabbar_control.rb +4 -0
  91. data/lib/ruflet_ui/ruflet/ui/controls/materials/tabbarview_control.rb +7 -0
  92. data/lib/ruflet_ui/ruflet/ui/controls/materials/tabs_control.rb +6 -0
  93. data/lib/ruflet_ui/ruflet/ui/controls/materials/textbutton_control.rb +5 -0
  94. data/lib/ruflet_ui/ruflet/ui/controls/materials/verticaldivider_control.rb +9 -0
  95. data/lib/ruflet_ui/ruflet/ui/controls/materials/video_control.rb +86 -1
  96. data/lib/ruflet_ui/ruflet/ui/controls/ruflet_controls.rb +24 -0
  97. data/lib/ruflet_ui/ruflet/ui/controls/shared/animatedswitcher_control.rb +10 -0
  98. data/lib/ruflet_ui/ruflet/ui/controls/shared/browsercontextmenu_control.rb +12 -1
  99. data/lib/ruflet_ui/ruflet/ui/controls/shared/canvas_control.rb +14 -0
  100. data/lib/ruflet_ui/ruflet/ui/controls/shared/dismissible_control.rb +19 -0
  101. data/lib/ruflet_ui/ruflet/ui/controls/shared/draggable_control.rb +9 -0
  102. data/lib/ruflet_ui/ruflet/ui/controls/shared/dragtarget_control.rb +6 -0
  103. data/lib/ruflet_ui/ruflet/ui/controls/shared/gridview_control.rb +20 -0
  104. data/lib/ruflet_ui/ruflet/ui/controls/shared/image_control.rb +19 -1
  105. data/lib/ruflet_ui/ruflet/ui/controls/shared/interactiveviewer_control.rb +37 -0
  106. data/lib/ruflet_ui/ruflet/ui/controls/shared/listview_control.rb +17 -0
  107. data/lib/ruflet_ui/ruflet/ui/controls/shared/pageview_control.rb +13 -0
  108. data/lib/ruflet_ui/ruflet/ui/controls/shared/placeholder_control.rb +12 -0
  109. data/lib/ruflet_ui/ruflet/ui/controls/shared/reorderabledraghandle_control.rb +4 -0
  110. data/lib/ruflet_ui/ruflet/ui/controls/shared/responsiverow_control.rb +22 -0
  111. data/lib/ruflet_ui/ruflet/ui/controls/shared/safearea_control.rb +11 -0
  112. data/lib/ruflet_ui/ruflet/ui/controls/shared/semantics_control.rb +2 -0
  113. data/lib/ruflet_ui/ruflet/ui/controls/shared/stack_control.rb +3 -0
  114. data/lib/ruflet_ui/ruflet/ui/controls/shared/text_control.rb +20 -1
  115. data/lib/ruflet_ui/ruflet/ui/controls/shared/view_control.rb +8 -0
  116. data/lib/ruflet_ui/ruflet/ui/controls/shared/window_control.rb +16 -0
  117. data/lib/ruflet_ui/ruflet/ui/controls/shared/windowdragarea_control.rb +6 -0
  118. data/lib/ruflet_ui/ruflet/ui/cupertino_control_methods.rb +83 -6
  119. data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +513 -33
  120. data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +2 -0
  121. data/lib/ruflet_ui/ruflet/ui/services/ruflet/audio_recorder_control.rb +87 -0
  122. data/lib/ruflet_ui/ruflet/ui/services/ruflet/geolocator_control.rb +85 -0
  123. data/lib/ruflet_ui/ruflet/ui/services/ruflet/permissionhandler_control.rb +55 -0
  124. data/lib/ruflet_ui/ruflet/ui/services/ruflet/securestorage_control.rb +91 -0
  125. data/lib/ruflet_ui/ruflet/ui/services/ruflet/semanticsservice_control.rb +28 -0
  126. data/lib/ruflet_ui/ruflet/ui/services/ruflet/tester_control.rb +126 -0
  127. data/lib/ruflet_ui/ruflet/ui/services/ruflet_services.rb +16 -0
  128. data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +230 -38
  129. data/lib/ruflet_ui.rb +4 -0
  130. metadata +9 -1
@@ -14,7 +14,132 @@ require "timeout"
14
14
 
15
15
  module Ruflet
16
16
  class Page
17
- PAGE_PROP_KEYS = %w[route title vertical_alignment horizontal_alignment scroll].freeze
17
+ class SharedPreferencesService
18
+ def initialize(page)
19
+ @page = page
20
+ end
21
+
22
+ def set(key, value, timeout: 10, on_result: nil)
23
+ invoke("set", { "key" => key, "value" => value }, timeout: timeout, on_result: on_result)
24
+ end
25
+
26
+ def get(key, timeout: 10, on_result: nil)
27
+ invoke("get", { "key" => key }, timeout: timeout, on_result: on_result)
28
+ end
29
+
30
+ def contains_key(key, timeout: 10, on_result: nil)
31
+ invoke("contains_key", { "key" => key }, timeout: timeout, on_result: on_result)
32
+ end
33
+
34
+ def get_keys(key_prefix, timeout: 10, on_result: nil)
35
+ invoke("get_keys", { "key_prefix" => key_prefix }, timeout: timeout, on_result: on_result)
36
+ end
37
+
38
+ def remove(key, timeout: 10, on_result: nil)
39
+ invoke("remove", { "key" => key }, timeout: timeout, on_result: on_result)
40
+ end
41
+
42
+ def clear(timeout: 10, on_result: nil)
43
+ invoke("clear", nil, timeout: timeout, on_result: on_result)
44
+ end
45
+
46
+ private
47
+
48
+ def invoke(method_name, args, timeout:, on_result:)
49
+ @page.__send__(:invoke_shared_preferences, method_name, args: args, timeout: timeout, on_result: on_result)
50
+ end
51
+ end
52
+
53
+ class WakelockService
54
+ def initialize(page)
55
+ @page = page
56
+ end
57
+
58
+ def enable(timeout: 10, on_result: nil)
59
+ invoke("enable", timeout: timeout, on_result: on_result)
60
+ end
61
+
62
+ def disable(timeout: 10, on_result: nil)
63
+ invoke("disable", timeout: timeout, on_result: on_result)
64
+ end
65
+
66
+ def is_enabled(timeout: 10, on_result: nil)
67
+ invoke("is_enabled", timeout: timeout, on_result: on_result)
68
+ end
69
+
70
+ private
71
+
72
+ def invoke(method_name, timeout:, on_result:)
73
+ @page.__send__(:invoke_wakelock, method_name, timeout: timeout, on_result: on_result)
74
+ end
75
+ end
76
+
77
+ class FlashlightService
78
+ def initialize(page)
79
+ @page = page
80
+ end
81
+
82
+ def on(timeout: 10, on_result: nil)
83
+ invoke("on", timeout: timeout, on_result: on_result)
84
+ end
85
+
86
+ def off(timeout: 10, on_result: nil)
87
+ invoke("off", timeout: timeout, on_result: on_result)
88
+ end
89
+
90
+ def is_available(timeout: 10, on_result: nil)
91
+ invoke("is_available", timeout: timeout, on_result: on_result)
92
+ end
93
+
94
+ private
95
+
96
+ def invoke(method_name, timeout:, on_result:)
97
+ @page.__send__(:invoke_flashlight, method_name, timeout: timeout, on_result: on_result)
98
+ end
99
+ end
100
+
101
+ class ScreenBrightnessService
102
+ def initialize(page)
103
+ @page = page
104
+ end
105
+
106
+ %w[
107
+ can_change_system_screen_brightness
108
+ get_application_screen_brightness
109
+ get_system_screen_brightness
110
+ is_animate
111
+ is_auto_reset
112
+ reset_application_screen_brightness
113
+ ].each do |method_name|
114
+ define_method(method_name) do |timeout: 10, on_result: nil|
115
+ invoke(method_name, nil, timeout: timeout, on_result: on_result)
116
+ end
117
+ end
118
+
119
+ def set_animate(animate, timeout: 10, on_result: nil)
120
+ invoke("set_animate", { "value" => animate }, timeout: timeout, on_result: on_result)
121
+ end
122
+
123
+ def set_auto_reset(auto_reset, timeout: 10, on_result: nil)
124
+ invoke("set_auto_reset", { "value" => auto_reset }, timeout: timeout, on_result: on_result)
125
+ end
126
+
127
+ def set_application_screen_brightness(brightness, timeout: 10, on_result: nil)
128
+ invoke("set_application_screen_brightness", { "value" => brightness }, timeout: timeout, on_result: on_result)
129
+ end
130
+
131
+ def set_system_screen_brightness(brightness, timeout: 10, on_result: nil)
132
+ invoke("set_system_screen_brightness", { "value" => brightness }, timeout: timeout, on_result: on_result)
133
+ end
134
+
135
+ private
136
+
137
+ def invoke(method_name, args, timeout:, on_result:)
138
+ @page.__send__(:invoke_screen_brightness, method_name, args: args, timeout: timeout, on_result: on_result)
139
+ end
140
+ end
141
+
142
+ PAGE_PROP_KEYS = %w[dark_theme fonts route rtl show_semantics_debugger theme theme_mode title vertical_alignment horizontal_alignment scroll].freeze
18
143
  DIALOG_PROP_KEYS = %w[dialog snack_bar bottom_sheet].freeze
19
144
  WIDGET_HELPER_METHODS = (
20
145
  Ruflet::UI::MaterialControlMethods.instance_methods(false) +
@@ -35,6 +160,10 @@ module Ruflet
35
160
  @root_controls = []
36
161
  @views = []
37
162
  @dialogs = []
163
+ @overlay_container_mounted = false
164
+ @dialogs_container_mounted = false
165
+ @services_container_mounted = false
166
+ @visual_service_controls = {}
38
167
  @page_event_handlers = {}
39
168
  @view_props = {}
40
169
  @page_props = { "route" => (client_details["route"] || "/") }
@@ -57,6 +186,10 @@ module Ruflet
57
186
  @invoke_waiters = {}
58
187
  @invoke_callbacks = {}
59
188
  @invoke_waiters_mutex = Mutex.new
189
+ @shared_preferences_proxy = SharedPreferencesService.new(self)
190
+ @wakelock_proxy = WakelockService.new(self)
191
+ @flashlight_proxy = FlashlightService.new(self)
192
+ @screen_brightness_proxy = ScreenBrightnessService.new(self)
60
193
  refresh_overlay_container!
61
194
  refresh_services_container!
62
195
  refresh_dialogs_container!
@@ -111,15 +244,16 @@ module Ruflet
111
244
  @view_props["bgcolor"] = normalize_value("bgcolor", value)
112
245
  end
113
246
 
114
- def add(*controls, appbar: nil, floating_action_button: nil, navigation_bar: nil, dialog: nil, snack_bar: nil, bottom_sheet: nil)
247
+ def add(*controls, appbar: nil, bottom_appbar: nil, floating_action_button: nil, navigation_bar: nil, dialog: nil, snack_bar: nil, bottom_sheet: nil)
115
248
  controls = controls.flatten
116
249
  visited = Set.new
117
250
  controls.each { |c| register_control_tree(c, visited) }
118
251
  @root_controls = controls
119
252
 
120
- @view_props["appbar"] = appbar if appbar
121
- @view_props["floating_action_button"] = floating_action_button if floating_action_button
122
- @view_props["navigation_bar"] = navigation_bar if navigation_bar
253
+ update_view_slot("appbar", appbar)
254
+ update_view_slot("bottom_appbar", bottom_appbar)
255
+ update_view_slot("floating_action_button", floating_action_button)
256
+ update_view_slot("navigation_bar", navigation_bar)
123
257
  @dialog = dialog if dialog
124
258
  @snack_bar = snack_bar if snack_bar
125
259
  @bottom_sheet = bottom_sheet if bottom_sheet
@@ -148,6 +282,50 @@ module Ruflet
148
282
  self
149
283
  end
150
284
 
285
+ def shared_preferences(**props)
286
+ return service(:shared_preferences, **props) unless props.empty?
287
+
288
+ @shared_preferences_proxy
289
+ end
290
+
291
+ def wakelock(**props)
292
+ return service(:wakelock, **props) unless props.empty?
293
+
294
+ @wakelock_proxy
295
+ end
296
+
297
+ def flashlight(**props)
298
+ return service(:flashlight, **props) unless props.empty?
299
+
300
+ @flashlight_proxy
301
+ end
302
+
303
+ def screen_brightness(**props)
304
+ return service(:screen_brightness, **props) unless props.empty?
305
+
306
+ @screen_brightness_proxy
307
+ end
308
+
309
+ def audio(**props)
310
+ service(:audio, **props)
311
+ end
312
+
313
+ def audio_recorder(**props)
314
+ service(:audio_recorder, **props)
315
+ end
316
+
317
+ def browser_context_menu(**props)
318
+ service(:browser_context_menu, **props)
319
+ end
320
+
321
+ def window(**props)
322
+ service(:window, **props)
323
+ end
324
+
325
+ def tester(**props)
326
+ service(:tester, **props)
327
+ end
328
+
151
329
  def add_service(*value)
152
330
  @services_container.props["_services"] = services + value.flatten.compact
153
331
  refresh_services_container!
@@ -180,12 +358,25 @@ module Ruflet
180
358
  mapped_props = normalize_props(props || {})
181
359
  id = mapped_props.delete("id")
182
360
  normalized_type = type.to_s.downcase
361
+ compact_type = normalized_type.delete("_")
362
+
363
+ if visual_service_type?(normalized_type)
364
+ key = id ? "id:#{id}" : normalized_type
365
+ existing = @visual_service_controls[key]
366
+ return existing if existing
367
+
368
+ svc = Ruflet::UI::ControlFactory.build(type.to_s, id: id&.to_s, **mapped_props)
369
+ @visual_service_controls[key] = svc
370
+ return svc
371
+ end
183
372
 
184
373
  existing =
185
374
  if id
186
375
  services.find { |s| s.is_a?(Control) && s.id.to_s == id.to_s }
187
376
  else
188
- services.find { |s| s.is_a?(Control) && s.type.to_s.downcase == normalized_type }
377
+ services.find do |s|
378
+ s.is_a?(Control) && s.type.to_s.downcase.delete("_") == compact_type
379
+ end
189
380
  end
190
381
  return existing if existing
191
382
 
@@ -201,6 +392,18 @@ module Ruflet
201
392
  self
202
393
  end
203
394
 
395
+ def navigate(route, **query_params)
396
+ go(route, **query_params)
397
+ end
398
+
399
+ def push_route(route, **query_params)
400
+ go(route, **query_params)
401
+ end
402
+
403
+ def query
404
+ parse_query(route)
405
+ end
406
+
204
407
  def on_route_change=(handler)
205
408
  @page_event_handlers["route_change"] = handler
206
409
  end
@@ -224,20 +427,70 @@ module Ruflet
224
427
  @view_props["appbar"] = value
225
428
  end
226
429
 
430
+ def bottom_appbar=(value)
431
+ @view_props["bottom_appbar"] = value
432
+ end
433
+
434
+ def bottomappbar=(value)
435
+ self.bottom_appbar = value
436
+ end
437
+
227
438
  def floating_action_button=(value)
228
439
  @view_props["floating_action_button"] = value
229
440
  end
230
441
 
442
+ def drawer
443
+ @view_props["drawer"]
444
+ end
445
+
446
+ def drawer=(value)
447
+ @view_props["drawer"] = value
448
+ end
449
+
450
+ def end_drawer
451
+ @view_props["end_drawer"]
452
+ end
453
+
454
+ def end_drawer=(value)
455
+ @view_props["end_drawer"] = value
456
+ end
457
+
458
+ def show_drawer(timeout: 10, on_result: nil)
459
+ raise ArgumentError, "show_drawer requires drawer" unless drawer
460
+
461
+ invoke_current_view("show_drawer", timeout: timeout, on_result: on_result)
462
+ self
463
+ end
464
+
465
+ def close_drawer(timeout: 10, on_result: nil)
466
+ invoke_current_view("close_drawer", timeout: timeout, on_result: on_result)
467
+ self
468
+ end
469
+
470
+ def show_end_drawer(timeout: 10, on_result: nil)
471
+ raise ArgumentError, "show_end_drawer requires end_drawer" unless end_drawer
472
+
473
+ invoke_current_view("show_end_drawer", timeout: timeout, on_result: on_result)
474
+ self
475
+ end
476
+
477
+ def close_end_drawer(timeout: 10, on_result: nil)
478
+ invoke_current_view("close_end_drawer", timeout: timeout, on_result: on_result)
479
+ self
480
+ end
481
+
231
482
  def dialog = @dialog
232
483
 
233
484
  def dialog=(value)
234
485
  @dialog = value
235
486
  refresh_dialogs_container!
487
+ push_dialogs_update! if @dialogs_container_mounted
236
488
  end
237
489
 
238
490
  def snack_bar=(value)
239
491
  @snack_bar = value
240
492
  refresh_dialogs_container!
493
+ push_dialogs_update! if @dialogs_container_mounted
241
494
  end
242
495
 
243
496
  def snackbar=(value)
@@ -259,6 +512,7 @@ module Ruflet
259
512
  return self if dialog_open?(dialog_control)
260
513
 
261
514
  dialog_control.props["open"] = true
515
+ remove_existing_singleton_dialogs(dialog_control)
262
516
  @dialogs << dialog_control unless @dialogs.include?(dialog_control)
263
517
  refresh_dialogs_container!
264
518
  send_view_patch unless @dialogs_container.wire_id
@@ -328,6 +582,30 @@ module Ruflet
328
582
  invoke(url_launcher, "can_launch_url", args: { "url" => url }, timeout: timeout)
329
583
  end
330
584
 
585
+ def close_in_app_web_view(timeout: 10, on_result: nil)
586
+ url_launcher = ensure_url_launcher_service
587
+ invoke(url_launcher, "close_in_app_web_view", timeout: timeout, on_result: on_result)
588
+ end
589
+
590
+ def open_window(url, title: nil, width: nil, height: nil, timeout: 10, on_result: nil)
591
+ url_launcher = ensure_url_launcher_service
592
+ args = { "url" => url }
593
+ args["title"] = title unless title.nil?
594
+ args["width"] = width unless width.nil?
595
+ args["height"] = height unless height.nil?
596
+ invoke(url_launcher, "open_window", args: args, timeout: timeout, on_result: on_result)
597
+ end
598
+
599
+ def supports_launch_mode(mode, timeout: 10, on_result: nil)
600
+ url_launcher = ensure_url_launcher_service
601
+ invoke(url_launcher, "supports_launch_mode", args: { "mode" => mode }, timeout: timeout, on_result: on_result)
602
+ end
603
+
604
+ def supports_close_for_launch_mode(mode, timeout: 10, on_result: nil)
605
+ url_launcher = ensure_url_launcher_service
606
+ invoke(url_launcher, "supports_close_for_launch_mode", args: { "mode" => mode }, timeout: timeout, on_result: on_result)
607
+ end
608
+
331
609
  # File picker helpers: create an ephemeral service, invoke method, and dispose it.
332
610
  def pick_files(
333
611
  dialog_title: nil,
@@ -341,14 +619,14 @@ module Ruflet
341
619
  )
342
620
  invoke_file_picker(
343
621
  "pick_files",
344
- {
622
+ compact_service_args(
345
623
  "dialog_title" => dialog_title,
346
624
  "initial_directory" => initial_directory,
347
625
  "file_type" => file_type,
348
626
  "allowed_extensions" => allowed_extensions,
349
627
  "allow_multiple" => allow_multiple,
350
628
  "with_data" => with_data
351
- },
629
+ ),
352
630
  timeout: timeout,
353
631
  on_result: on_result
354
632
  )
@@ -366,14 +644,14 @@ module Ruflet
366
644
  )
367
645
  invoke_file_picker(
368
646
  "save_file",
369
- {
647
+ compact_service_args(
370
648
  "dialog_title" => dialog_title,
371
649
  "file_name" => file_name,
372
650
  "initial_directory" => initial_directory,
373
651
  "file_type" => file_type,
374
652
  "allowed_extensions" => allowed_extensions,
375
653
  "src_bytes" => src_bytes
376
- },
654
+ ),
377
655
  timeout: timeout,
378
656
  on_result: on_result
379
657
  )
@@ -382,18 +660,212 @@ module Ruflet
382
660
  def get_directory_path(dialog_title: nil, initial_directory: nil, timeout: nil, on_result: nil)
383
661
  invoke_file_picker(
384
662
  "get_directory_path",
385
- {
663
+ compact_service_args(
386
664
  "dialog_title" => dialog_title,
387
665
  "initial_directory" => initial_directory
388
- },
666
+ ),
667
+ timeout: timeout,
668
+ on_result: on_result
669
+ )
670
+ end
671
+
672
+ def upload(files, timeout: nil, on_result: nil)
673
+ invoke_file_picker(
674
+ "upload",
675
+ { "files" => Array(files).map { |file| normalize_service_value(file) } },
389
676
  timeout: timeout,
390
677
  on_result: on_result
391
678
  )
392
679
  end
393
680
 
681
+ def upload_files(files, timeout: nil, on_result: nil)
682
+ upload(files, timeout: timeout, on_result: on_result)
683
+ end
684
+
685
+ def disable_browser_context_menu(timeout: 10, on_result: nil)
686
+ invoke_browser_context_menu("disable_menu", timeout: timeout, on_result: on_result)
687
+ end
688
+
689
+ def enable_browser_context_menu(timeout: 10, on_result: nil)
690
+ invoke_browser_context_menu("enable_menu", timeout: timeout, on_result: on_result)
691
+ end
692
+
693
+ def wait_until_ready_to_show(timeout: 10, on_result: nil)
694
+ invoke_window("wait_until_ready_to_show", timeout: timeout, on_result: on_result)
695
+ end
696
+
697
+ def window_to_front(timeout: 10, on_result: nil)
698
+ invoke_window("to_front", timeout: timeout, on_result: on_result)
699
+ end
700
+
701
+ def center_window(timeout: 10, on_result: nil)
702
+ invoke_window("center", timeout: timeout, on_result: on_result)
703
+ end
704
+
705
+ def close_window(timeout: 10, on_result: nil)
706
+ invoke_window("close", timeout: timeout, on_result: on_result)
707
+ end
708
+
709
+ def destroy_window(timeout: 10, on_result: nil)
710
+ invoke_window("destroy", timeout: timeout, on_result: on_result)
711
+ end
712
+
713
+ def start_window_dragging(timeout: 10, on_result: nil)
714
+ invoke_window("start_dragging", timeout: timeout, on_result: on_result)
715
+ end
716
+
717
+ def start_window_resizing(edge, timeout: 10, on_result: nil)
718
+ invoke_window(
719
+ "start_resizing",
720
+ args: { "edge" => normalize_service_value(edge) },
721
+ timeout: timeout,
722
+ on_result: on_result
723
+ )
724
+ end
725
+
726
+ def tester_pump(options = nil, duration: nil, timeout: 10, on_result: nil)
727
+ duration = options[:duration] || options["duration"] if options.is_a?(Hash) && duration.nil?
728
+ invoke_tester("pump", args: compact_service_args("duration" => duration), timeout: timeout, on_result: on_result)
729
+ end
730
+
731
+ def tester_pump_and_settle(options = nil, duration: nil, timeout: 10, on_result: nil)
732
+ duration = options[:duration] || options["duration"] if options.is_a?(Hash) && duration.nil?
733
+ invoke_tester("pump_and_settle", args: compact_service_args("duration" => duration), timeout: timeout, on_result: on_result)
734
+ end
735
+
736
+ def find_by_text(text, timeout: 10, on_result: nil)
737
+ invoke_tester("find_by_text", args: { "text" => text }, timeout: timeout, on_result: on_result)
738
+ end
739
+
740
+ def find_by_text_containing(pattern, timeout: 10, on_result: nil)
741
+ invoke_tester("find_by_text_containing", args: { "pattern" => pattern }, timeout: timeout, on_result: on_result)
742
+ end
743
+
744
+ def find_by_key(key, timeout: 10, on_result: nil)
745
+ invoke_tester("find_by_key", args: { "key" => key }, timeout: timeout, on_result: on_result)
746
+ end
747
+
748
+ def find_by_tooltip(value, timeout: 10, on_result: nil)
749
+ invoke_tester("find_by_tooltip", args: { "value" => value }, timeout: timeout, on_result: on_result)
750
+ end
751
+
752
+ def find_by_icon(icon, timeout: 10, on_result: nil)
753
+ invoke_tester("find_by_icon", args: { "icon" => normalize_service_value(icon) }, timeout: timeout, on_result: on_result)
754
+ end
755
+
756
+ def take_screenshot(name, timeout: 10, on_result: nil)
757
+ invoke_tester("take_screenshot", args: { "name" => name }, timeout: timeout, on_result: on_result)
758
+ end
759
+
760
+ def tap(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
761
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
762
+ invoke_tester_finder("tap", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
763
+ end
764
+
765
+ def mouse_click(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
766
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
767
+ invoke_tester_finder("mouse_click", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
768
+ end
769
+
770
+ def mouse_double_click(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
771
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
772
+ invoke_tester_finder("mouse_double_click", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
773
+ end
774
+
775
+ def right_mouse_click(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
776
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
777
+ invoke_tester_finder("right_mouse_click", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
778
+ end
779
+
780
+ def tap_at(offset = nil, timeout: 10, on_result: nil)
781
+ invoke_tester_at("tap_at", offset, timeout: timeout, on_result: on_result)
782
+ end
783
+
784
+ def mouse_click_at(offset = nil, timeout: 10, on_result: nil)
785
+ invoke_tester_at("mouse_click_at", offset, timeout: timeout, on_result: on_result)
786
+ end
787
+
788
+ def mouse_double_click_at(offset = nil, timeout: 10, on_result: nil)
789
+ invoke_tester_at("mouse_double_click_at", offset, timeout: timeout, on_result: on_result)
790
+ end
791
+
792
+ def right_mouse_click_at(offset = nil, timeout: 10, on_result: nil)
793
+ invoke_tester_at("right_mouse_click_at", offset, timeout: timeout, on_result: on_result)
794
+ end
795
+
796
+ def drag(finder_id, offset, finder_index: nil, timeout: 10, on_result: nil)
797
+ invoke_tester(
798
+ "drag",
799
+ args: compact_service_args(
800
+ "finder_id" => finder_id,
801
+ "finder_index" => finder_index,
802
+ "offset" => offset
803
+ ),
804
+ timeout: timeout,
805
+ on_result: on_result
806
+ )
807
+ end
808
+
809
+ def drag_from(start, offset, timeout: 10, on_result: nil)
810
+ invoke_tester(
811
+ "drag_from",
812
+ args: compact_service_args("start" => start, "offset" => offset),
813
+ timeout: timeout,
814
+ on_result: on_result
815
+ )
816
+ end
817
+
818
+ def long_press(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
819
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
820
+ invoke_tester_finder("long_press", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
821
+ end
822
+
823
+ def enter_text(finder_id, text, options = nil, finder_index: nil, timeout: 10, on_result: nil)
824
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
825
+ invoke_tester(
826
+ "enter_text",
827
+ args: compact_service_args(
828
+ "finder_id" => finder_id,
829
+ "finder_index" => finder_index,
830
+ "text" => text
831
+ ),
832
+ timeout: timeout,
833
+ on_result: on_result
834
+ )
835
+ end
836
+
837
+ def mouse_hover(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
838
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
839
+ invoke_tester_finder("mouse_hover", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
840
+ end
841
+
842
+ def tester_teardown(timeout: 10, on_result: nil)
843
+ invoke_tester("teardown", timeout: timeout, on_result: on_result)
844
+ end
845
+
846
+ def heavy_impact(timeout: 10, on_result: nil)
847
+ invoke_haptic_feedback("heavy_impact", timeout: timeout, on_result: on_result)
848
+ end
849
+
850
+ def medium_impact(timeout: 10, on_result: nil)
851
+ invoke_haptic_feedback("medium_impact", timeout: timeout, on_result: on_result)
852
+ end
853
+
854
+ def light_impact(timeout: 10, on_result: nil)
855
+ invoke_haptic_feedback("light_impact", timeout: timeout, on_result: on_result)
856
+ end
857
+
858
+ def selection_click(timeout: 10, on_result: nil)
859
+ invoke_haptic_feedback("selection_click", timeout: timeout, on_result: on_result)
860
+ end
861
+
862
+ def vibrate(timeout: 10, on_result: nil)
863
+ invoke_haptic_feedback("vibrate", timeout: timeout, on_result: on_result)
864
+ end
865
+
394
866
  def set_clipboard(value, timeout: nil, on_result: nil)
395
867
  invoke_clipboard_method(
396
- "set_data",
868
+ "set",
397
869
  args: { "data" => value.to_s },
398
870
  timeout: timeout,
399
871
  on_result: on_result
@@ -401,7 +873,7 @@ module Ruflet
401
873
  end
402
874
 
403
875
  def get_clipboard(timeout: nil, on_result: nil)
404
- invoke_clipboard_method("get_data", timeout: timeout, on_result: on_result)
876
+ invoke_clipboard_method("get", timeout: timeout, on_result: on_result)
405
877
  end
406
878
 
407
879
  def set_clipboard_files(files, timeout: nil, on_result: nil)
@@ -442,10 +914,94 @@ module Ruflet
442
914
  invoke_battery_method("get_battery_state", timeout: timeout, on_result: on_result)
443
915
  end
444
916
 
445
- def battery_save_mode?(timeout: nil, on_result: nil)
917
+ def is_in_battery_save_mode(timeout: nil, on_result: nil)
446
918
  invoke_battery_method("is_in_battery_save_mode", timeout: timeout, on_result: on_result)
447
919
  end
448
920
 
921
+ def battery_save_mode?(timeout: nil, on_result: nil)
922
+ is_in_battery_save_mode(timeout: timeout, on_result: on_result)
923
+ end
924
+
925
+ def accelerometer(**props)
926
+ service(:accelerometer, **props)
927
+ end
928
+
929
+ def gyroscope(**props)
930
+ service(:gyroscope, **props)
931
+ end
932
+
933
+ def user_accelerometer(**props)
934
+ service(:user_accelerometer, **props)
935
+ end
936
+
937
+ def magnetometer(**props)
938
+ service(:magnetometer, **props)
939
+ end
940
+
941
+ def barometer(**props)
942
+ service(:barometer, **props)
943
+ end
944
+
945
+ def shake_detector(**props)
946
+ service(:shake_detector, **props)
947
+ end
948
+
949
+ def semantics_service(**props)
950
+ service(:semantics_service, **props)
951
+ end
952
+
953
+ def screenshot(**props)
954
+ service(:screenshot, **props)
955
+ end
956
+
957
+ def battery(**props)
958
+ service(:battery, **props)
959
+ end
960
+
961
+ def connectivity(**props)
962
+ service(:connectivity, **props)
963
+ end
964
+
965
+ def clipboard(**props)
966
+ service(:clipboard, **props)
967
+ end
968
+
969
+ def file_picker(**props)
970
+ service(:file_picker, **props)
971
+ end
972
+
973
+ def url_launcher(**props)
974
+ service(:url_launcher, **props)
975
+ end
976
+
977
+ def storage_paths(**props)
978
+ service(:storage_paths, **props)
979
+ end
980
+
981
+ def share(**props)
982
+ service(:share, **props)
983
+ end
984
+
985
+ def camera(**props)
986
+ service(:camera, **props)
987
+ end
988
+
989
+ def haptic_feedback(**props)
990
+ service(:haptic_feedback, **props)
991
+ end
992
+
993
+ def geolocator(**props)
994
+ service(:geolocator, **props)
995
+ end
996
+
997
+ def permission_handler(**props)
998
+ service(:permission_handler, **props)
999
+ end
1000
+
1001
+ def secure_storage(**props)
1002
+ service(:secure_storage, **props)
1003
+ end
1004
+
449
1005
  def get_application_cache_directory(timeout: nil, on_result: nil)
450
1006
  invoke_storage_paths("get_application_cache_directory", timeout: timeout, on_result: on_result)
451
1007
  end
@@ -487,7 +1043,7 @@ module Ruflet
487
1043
  end
488
1044
 
489
1045
  def share_text(
490
- text:,
1046
+ text = nil,
491
1047
  title: nil,
492
1048
  subject: nil,
493
1049
  preview_thumbnail: nil,
@@ -502,7 +1058,7 @@ module Ruflet
502
1058
  invoke(
503
1059
  share,
504
1060
  "share_text",
505
- args: {
1061
+ args: compact_service_args(
506
1062
  "text" => text,
507
1063
  "title" => title,
508
1064
  "subject" => subject,
@@ -511,14 +1067,14 @@ module Ruflet
511
1067
  "download_fallback_enabled" => download_fallback_enabled,
512
1068
  "mail_to_fallback_enabled" => mail_to_fallback_enabled,
513
1069
  "excluded_cupertino_activities" => excluded_cupertino_activities
514
- },
1070
+ ),
515
1071
  timeout: timeout,
516
1072
  on_result: on_result
517
1073
  )
518
1074
  end
519
1075
 
520
1076
  def share_uri(
521
- uri:,
1077
+ uri = nil,
522
1078
  share_position_origin: nil,
523
1079
  excluded_cupertino_activities: nil,
524
1080
  timeout: nil,
@@ -528,18 +1084,18 @@ module Ruflet
528
1084
  invoke(
529
1085
  share,
530
1086
  "share_uri",
531
- args: {
1087
+ args: compact_service_args(
532
1088
  "uri" => uri,
533
1089
  "share_position_origin" => share_position_origin,
534
1090
  "excluded_cupertino_activities" => excluded_cupertino_activities
535
- },
1091
+ ),
536
1092
  timeout: timeout,
537
1093
  on_result: on_result
538
1094
  )
539
1095
  end
540
1096
 
541
1097
  def share_files(
542
- files:,
1098
+ files = nil,
543
1099
  text: nil,
544
1100
  title: nil,
545
1101
  subject: nil,
@@ -555,17 +1111,17 @@ module Ruflet
555
1111
  invoke(
556
1112
  share,
557
1113
  "share_files",
558
- args: {
559
- "files" => files,
1114
+ args: compact_service_args(
1115
+ "files" => normalize_share_files(files),
560
1116
  "text" => text,
561
1117
  "title" => title,
562
1118
  "subject" => subject,
563
- "preview_thumbnail" => preview_thumbnail,
1119
+ "preview_thumbnail" => normalize_share_file(preview_thumbnail),
564
1120
  "share_position_origin" => share_position_origin,
565
1121
  "download_fallback_enabled" => download_fallback_enabled,
566
1122
  "mail_to_fallback_enabled" => mail_to_fallback_enabled,
567
1123
  "excluded_cupertino_activities" => excluded_cupertino_activities
568
- },
1124
+ ),
569
1125
  timeout: timeout,
570
1126
  on_result: on_result
571
1127
  )
@@ -594,11 +1150,22 @@ module Ruflet
594
1150
  return nil unless dialog_control
595
1151
 
596
1152
  dialog_control.props["open"] = false
597
- refresh_dialogs_container!
598
- push_dialogs_update!
1153
+ update(dialog_control, open: false)
599
1154
  dialog_control
600
1155
  end
601
1156
 
1157
+ def close_dialog(dialog_control)
1158
+ return self unless dialog_control
1159
+
1160
+ before_dialog_count = @dialogs_container.props["controls"].length
1161
+ dialog_control.props["open"] = false
1162
+ @dialog = nil if @dialog.equal?(dialog_control)
1163
+ remove_dialog_tracking(dialog_control)
1164
+ refresh_dialogs_container!
1165
+ push_dialogs_update!(force_view: @dialogs_container.props["controls"].length < before_dialog_count)
1166
+ self
1167
+ end
1168
+
602
1169
  def update(control_or_id = nil, **props)
603
1170
  if control_or_id.nil? && props.empty?
604
1171
  send_view_patch
@@ -634,6 +1201,7 @@ module Ruflet
634
1201
 
635
1202
  visited = Set.new
636
1203
  patch.each_value { |value| register_embedded_value(value, visited) }
1204
+ patch.each { |k, v| control.props[k] = v }
637
1205
 
638
1206
  patch_ops = patch.map { |k, v| [0, 0, k, serialize_patch_value(v)] }
639
1207
 
@@ -656,8 +1224,6 @@ module Ruflet
656
1224
  patch = normalize_props(props || {})
657
1225
  patch.each { |k, v| control.props[k] = v }
658
1226
 
659
- remove_dialog_tracking(control) if patch.key?("open") && patch["open"] == false
660
-
661
1227
  self
662
1228
  end
663
1229
 
@@ -675,11 +1241,13 @@ module Ruflet
675
1241
  return unless control
676
1242
 
677
1243
  event = Event.new(name: name, target: target, raw_data: data, page: self, control: control)
678
- control.emit(name, event)
679
-
680
- if name.to_s == "dismiss" && remove_dialog_tracking(control)
681
- push_dialogs_update!
1244
+ apply_event_value_to_control(control, event) if %w[change select select_change].include?(name.to_s)
1245
+ before_dialog_count = @dialogs_container.props["controls"].length
1246
+ if dialog_close_event?(control, name) && remove_dialog_tracking(control)
1247
+ push_dialogs_update!(force_view: @dialogs_container.props["controls"].length < before_dialog_count)
682
1248
  end
1249
+
1250
+ control.emit(name, event)
683
1251
  end
684
1252
 
685
1253
  def method_missing(name, *args, &block)
@@ -754,10 +1322,56 @@ module Ruflet
754
1322
 
755
1323
  def build_widget(type, **props, &block) = WidgetBuilder.new.control(type, **props, &block)
756
1324
 
1325
+ def compact_service_args(hash)
1326
+ hash.each_with_object({}) do |(key, value), result|
1327
+ result[key] = normalize_service_value(value) unless value.nil?
1328
+ end
1329
+ end
1330
+
1331
+ def normalize_service_value(value)
1332
+ case value
1333
+ when Array
1334
+ value.map { |item| normalize_service_value(item) }
1335
+ when Hash
1336
+ value.transform_keys(&:to_s).each_with_object({}) do |(key, item), result|
1337
+ next if item.nil?
1338
+
1339
+ result[key] = key == "data" && byte_array?(item) ? item.pack("C*").b : normalize_service_value(item)
1340
+ end
1341
+ else
1342
+ value
1343
+ end
1344
+ end
1345
+
1346
+ def normalize_share_files(files)
1347
+ return nil if files.nil?
1348
+
1349
+ Array(files).map { |file| normalize_share_file(file) }
1350
+ end
1351
+
1352
+ def normalize_share_file(file)
1353
+ case file
1354
+ when nil
1355
+ nil
1356
+ when String
1357
+ { "path" => file }
1358
+ else
1359
+ normalize_service_value(file)
1360
+ end
1361
+ end
1362
+
1363
+ def byte_array?(value)
1364
+ value.is_a?(Array) && value.all? { |item| item.is_a?(Integer) && item.between?(0, 255) }
1365
+ end
1366
+
757
1367
  def widget_helper_method?(name)
758
1368
  WIDGET_HELPER_METHODS.include?(name.to_s)
759
1369
  end
760
1370
 
1371
+ def visual_service_type?(type)
1372
+ type.to_s.delete("_") == "camera"
1373
+ end
1374
+
761
1375
  def text_maps_to_content?(control, patch)
762
1376
  patch.key?("text") && control.type.end_with?("button")
763
1377
  end
@@ -772,6 +1386,14 @@ module Ruflet
772
1386
  @sender.call(action, payload)
773
1387
  end
774
1388
 
1389
+ def update_view_slot(name, value)
1390
+ if value.nil?
1391
+ @view_props.delete(name)
1392
+ else
1393
+ @view_props[name] = value
1394
+ end
1395
+ end
1396
+
775
1397
  def send_view_patch
776
1398
  refresh_control_indexes!
777
1399
  view_patches = build_view_patches
@@ -781,10 +1403,13 @@ module Ruflet
781
1403
  "id" => 1,
782
1404
  "patch" => [
783
1405
  [0],
784
- [0, 0, "views", view_patches],
785
- *page_patch_ops
1406
+ *page_patch_ops,
1407
+ [0, 0, "views", view_patches]
786
1408
  ]
787
1409
  })
1410
+ @overlay_container_mounted = true if @overlay_container.wire_id
1411
+ @dialogs_container_mounted = true if @dialogs_container.wire_id
1412
+ @services_container_mounted = true if @services_container.wire_id
788
1413
  end
789
1414
 
790
1415
  def register_control_tree(control, visited = Set.new)
@@ -895,6 +1520,15 @@ module Ruflet
895
1520
  "#{base}#{separator}#{query}"
896
1521
  end
897
1522
 
1523
+ def parse_query(route_value)
1524
+ query_string = route_value.to_s.split("?", 2)[1].to_s
1525
+ return {} if query_string.empty?
1526
+
1527
+ CGI.parse(query_string).each_with_object({}) do |(key, values), result|
1528
+ result[key] = values.size == 1 ? values.first : values
1529
+ end
1530
+ end
1531
+
898
1532
  def extract_route(data)
899
1533
  case data
900
1534
  when String
@@ -910,10 +1544,46 @@ module Ruflet
910
1544
  handler = @page_event_handlers[name.to_s.sub(/\Aon_/, "")]
911
1545
  return unless handler.respond_to?(:call)
912
1546
 
913
- event = Event.new(name: name.to_s, target: 1, raw_data: data, page: self, control: nil)
1547
+ event = Ruflet::Event.new(name: name.to_s, target: 1, raw_data: data, page: self, control: nil)
914
1548
  handler.call(event)
915
1549
  end
916
1550
 
1551
+ def apply_event_value_to_control(control, event)
1552
+ return unless event.typed_data && event.typed_data.respond_to?(:value)
1553
+
1554
+ value = event.typed_data.value
1555
+ if control.props.key?("start_value") && control.props.key?("end_value")
1556
+ raw = event.typed_data.respond_to?(:raw) ? event.typed_data.raw : event.data
1557
+ range_value = value.is_a?(Hash) ? value : raw
1558
+ if range_value.is_a?(Hash)
1559
+ start_value = range_value["start_value"] || range_value[:start_value]
1560
+ end_value = range_value["end_value"] || range_value[:end_value]
1561
+ control.props["start_value"] = start_value unless start_value.nil?
1562
+ control.props["end_value"] = end_value unless end_value.nil?
1563
+ end
1564
+ return
1565
+ end
1566
+
1567
+ return if value.nil?
1568
+ return if control.type == "selectionarea"
1569
+
1570
+ prop_name =
1571
+ if event.name == "select"
1572
+ control.props.key?("value") ? "value" : "selected"
1573
+ elsif event.name == "select_change" && control.props.key?("selected")
1574
+ "selected"
1575
+ elsif control.props.key?("expanded")
1576
+ "expanded"
1577
+ elsif control.props.key?("selected")
1578
+ "selected"
1579
+ elsif control.props.key?("selected_index")
1580
+ "selected_index"
1581
+ else
1582
+ "value"
1583
+ end
1584
+ control.props[prop_name] = value
1585
+ end
1586
+
917
1587
  def page_control_target?(control_or_id)
918
1588
  control_or_id == 1 || control_or_id.to_s == "1" || control_or_id.to_s == "page"
919
1589
  end
@@ -927,7 +1597,7 @@ module Ruflet
927
1597
  when Array
928
1598
  value.map { |v| serialize_patch_value(v) }
929
1599
  when Hash
930
- value.transform_values { |v| serialize_patch_value(v) }
1600
+ value.each_with_object({}) { |(k, v), result| result[k.to_s] = serialize_patch_value(v) }
931
1601
  else
932
1602
  value
933
1603
  end
@@ -938,7 +1608,7 @@ module Ruflet
938
1608
  end
939
1609
 
940
1610
  def refresh_dialogs_container!
941
- dialog_controls = (@dialogs + dialog_slots).uniq
1611
+ dialog_controls = (dialog_slots + @dialogs).uniq
942
1612
  @dialogs_container.props["controls"] = dialog_controls
943
1613
  @page_props["_dialogs"] = @dialogs_container
944
1614
  end
@@ -964,9 +1634,15 @@ module Ruflet
964
1634
  end
965
1635
  end
966
1636
 
967
- def push_dialogs_update!
1637
+ def push_dialogs_update!(force_view: false)
968
1638
  refresh_control_indexes!
969
1639
 
1640
+ if force_view || @dialogs_container.props["controls"].empty?
1641
+ @dialogs_container_mounted = false
1642
+ send_view_patch
1643
+ return
1644
+ end
1645
+
970
1646
  if @dialogs_container.wire_id
971
1647
  send_message(Protocol::ACTIONS[:patch_control], {
972
1648
  "id" => @dialogs_container.wire_id,
@@ -989,6 +1665,11 @@ module Ruflet
989
1665
  @dialogs.include?(dialog_control) && dialog_control.props["open"] == true
990
1666
  end
991
1667
 
1668
+ def dialog_close_event?(control, name)
1669
+ name = name.to_s
1670
+ name == "dismiss" || (%w[change select select_change].include?(name) && @dialogs.include?(control) && control.props["open"] == false)
1671
+ end
1672
+
992
1673
  def remove_dialog_tracking(control)
993
1674
  return false unless @dialogs.include?(control)
994
1675
 
@@ -997,6 +1678,16 @@ module Ruflet
997
1678
  true
998
1679
  end
999
1680
 
1681
+ def remove_existing_singleton_dialogs(control)
1682
+ return unless singleton_dialog_control?(control)
1683
+
1684
+ @dialogs.delete_if { |dialog| dialog != control && singleton_dialog_control?(dialog) }
1685
+ end
1686
+
1687
+ def singleton_dialog_control?(control)
1688
+ control.type.to_s.tr("_", "").downcase == "snackbar"
1689
+ end
1690
+
1000
1691
  def assign_split_prop(key, value)
1001
1692
  if key == "vertical_alignment" || key == "horizontal_alignment"
1002
1693
  @page_props[key] = value
@@ -1024,202 +1715,233 @@ module Ruflet
1024
1715
  # Keep internal containers stable after initial mount.
1025
1716
  # Re-sending them as full objects can replace Control instances with
1026
1717
  # same IDs and detach service invoke listeners on the Flutter side.
1027
- next nil if k == "_overlay" && @overlay_container.wire_id
1028
- next nil if k == "_dialogs" && @dialogs_container.wire_id
1718
+ next nil if k == "_overlay" && @overlay_container_mounted
1719
+ next nil if k == "_dialogs" && @dialogs_container_mounted
1720
+ next nil if k == "_services" && @services_container_mounted
1029
1721
 
1030
1722
  [0, 0, k, serialize_patch_value(v)]
1031
1723
  end
1032
1724
  end
1033
1725
 
1034
- def ensure_clipboard_service
1035
- clipboard = services.find { |service| service.is_a?(Control) && service.type == "clipboard" }
1036
- return [clipboard, false] if clipboard
1726
+ def service_by_type(type)
1727
+ compact_type = type.to_s.downcase.delete("_")
1728
+ services.find do |service|
1729
+ service.is_a?(Control) && service.type.to_s.downcase.delete("_") == compact_type
1730
+ end
1731
+ end
1037
1732
 
1038
- clipboard = build_widget(:clipboard)
1039
- add_service(clipboard)
1040
- [clipboard, true]
1733
+ def service_with_created(type)
1734
+ existing = service_by_type(type)
1735
+ return [existing, false] if existing
1736
+
1737
+ [service(type), true]
1738
+ end
1739
+
1740
+ def ensure_clipboard_service
1741
+ service_with_created(:clipboard)
1041
1742
  end
1042
1743
 
1043
1744
  def invoke_clipboard_method(method_name, args: nil, timeout:, on_result:)
1044
- clipboard, created = ensure_clipboard_service
1045
- send_view_patch if created
1046
- sleep(0.05) if created
1745
+ clipboard, = ensure_clipboard_service
1047
1746
  invoke(
1048
1747
  clipboard,
1049
1748
  method_name,
1050
1749
  args: args,
1051
1750
  timeout: timeout,
1052
- on_result: lambda { |result, error|
1053
- message = error.to_s
1054
- if message.include?("inexistent control")
1055
- remove_service(clipboard)
1056
- fresh_clipboard, = ensure_clipboard_service
1057
- sleep(0.08)
1058
- invoke(
1059
- fresh_clipboard,
1060
- method_name,
1061
- args: args,
1062
- timeout: timeout,
1063
- on_result: on_result
1064
- )
1065
- else
1066
- on_result&.call(result, error)
1067
- end
1068
- }
1751
+ on_result: on_result
1069
1752
  )
1070
1753
  rescue StandardError => e
1071
1754
  on_result&.call(nil, e.message)
1072
1755
  end
1073
1756
 
1074
1757
  def ensure_url_launcher_service
1075
- url_launcher = services.find { |service| service.is_a?(Control) && %w[urllauncher url_launcher].include?(service.type) }
1076
- return url_launcher if url_launcher
1758
+ service(:url_launcher)
1759
+ end
1077
1760
 
1078
- url_launcher = build_widget(:url_launcher)
1079
- add_service(url_launcher)
1080
- url_launcher
1761
+ def ensure_browser_context_menu_service
1762
+ service(:browser_context_menu)
1081
1763
  end
1082
1764
 
1083
- def ensure_connectivity_service
1084
- connectivity = services.find { |service| service.is_a?(Control) && service.type == "connectivity" }
1085
- return [connectivity, false] if connectivity
1765
+ def invoke_browser_context_menu(method_name, timeout:, on_result:)
1766
+ browser_context_menu = ensure_browser_context_menu_service
1767
+ invoke(browser_context_menu, method_name, timeout: timeout, on_result: on_result)
1768
+ end
1769
+
1770
+ def ensure_window_service
1771
+ service(:window)
1772
+ end
1773
+
1774
+ def invoke_window(method_name, args: nil, timeout:, on_result:)
1775
+ window = ensure_window_service
1776
+ invoke(window, method_name, args: args, timeout: timeout, on_result: on_result)
1777
+ end
1778
+
1779
+ def ensure_tester_service
1780
+ service(:tester)
1781
+ end
1782
+
1783
+ def invoke_tester(method_name, args: nil, timeout:, on_result:)
1784
+ tester = ensure_tester_service
1785
+ invoke(tester, method_name, args: args, timeout: timeout, on_result: on_result)
1786
+ end
1086
1787
 
1087
- connectivity = build_widget(:connectivity)
1088
- add_service(connectivity)
1089
- [connectivity, true]
1788
+ def invoke_tester_finder(method_name, finder_id, finder_index:, timeout:, on_result:)
1789
+ invoke_tester(
1790
+ method_name,
1791
+ args: compact_service_args("finder_id" => finder_id, "finder_index" => finder_index),
1792
+ timeout: timeout,
1793
+ on_result: on_result
1794
+ )
1795
+ end
1796
+
1797
+ def invoke_tester_at(method_name, offset, timeout:, on_result:)
1798
+ invoke_tester(
1799
+ method_name,
1800
+ args: compact_service_args("offset" => offset),
1801
+ timeout: timeout,
1802
+ on_result: on_result
1803
+ )
1804
+ end
1805
+
1806
+ def ensure_haptic_feedback_service
1807
+ service(:haptic_feedback)
1808
+ end
1809
+
1810
+ def invoke_haptic_feedback(method_name, timeout:, on_result:)
1811
+ haptic_feedback = ensure_haptic_feedback_service
1812
+ invoke(haptic_feedback, method_name, timeout: timeout, on_result: on_result)
1813
+ end
1814
+
1815
+ def invoke_current_view(method_name, timeout:, on_result:)
1816
+ target_id = @views.last&.wire_id || @view_id
1817
+ invoke_control_id(target_id, method_name, timeout: timeout, on_result: on_result)
1818
+ end
1819
+
1820
+ def invoke_control_id(control_id, method_name, args: nil, timeout: 10, on_result: nil)
1821
+ call_id = "call_#{Ruflet::Control.generate_id}"
1822
+ if on_result.respond_to?(:call)
1823
+ @invoke_waiters_mutex.synchronize { @invoke_callbacks[call_id] = on_result }
1824
+ unless timeout.nil?
1825
+ Thread.new(call_id, timeout.to_f) do |pending_call_id, invoke_timeout|
1826
+ sleep([invoke_timeout, 0.0].max + 0.1)
1827
+ callback = @invoke_waiters_mutex.synchronize { @invoke_callbacks.delete(pending_call_id) }
1828
+ callback&.call(nil, "execution expired")
1829
+ rescue StandardError => e
1830
+ Kernel.warn("invoke timeout callback error: #{e.class}: #{e.message}")
1831
+ end
1832
+ end
1833
+ end
1834
+
1835
+ payload = {
1836
+ "control_id" => control_id,
1837
+ "call_id" => call_id,
1838
+ "name" => method_name.to_s,
1839
+ "args" => args
1840
+ }
1841
+ payload["timeout"] = timeout unless timeout.nil?
1842
+ send_message(Protocol::ACTIONS[:invoke_control_method], payload)
1843
+
1844
+ call_id
1845
+ end
1846
+
1847
+ def ensure_connectivity_service
1848
+ service_with_created(:connectivity)
1090
1849
  end
1091
1850
 
1092
1851
  def invoke_connectivity_method(method_name, timeout:, on_result:)
1093
- connectivity, created = ensure_connectivity_service
1094
- send_view_patch if created
1095
- sleep(0.05) if created
1852
+ connectivity, = ensure_connectivity_service
1096
1853
  invoke(
1097
1854
  connectivity,
1098
1855
  method_name,
1099
1856
  timeout: timeout,
1100
- on_result: lambda { |result, error|
1101
- message = error.to_s
1102
- if message.include?("inexistent control")
1103
- remove_service(connectivity)
1104
- fresh_connectivity, = ensure_connectivity_service
1105
- sleep(0.08)
1106
- invoke(
1107
- fresh_connectivity,
1108
- method_name,
1109
- timeout: timeout,
1110
- on_result: on_result
1111
- )
1112
- else
1113
- on_result&.call(result, error)
1114
- end
1115
- }
1857
+ on_result: on_result
1116
1858
  )
1117
1859
  rescue StandardError => e
1118
1860
  on_result&.call(nil, e.message)
1119
1861
  end
1120
1862
 
1121
1863
  def ensure_battery_service
1122
- battery = services.find { |service| service.is_a?(Control) && service.type == "battery" }
1123
- return [battery, false] if battery
1124
-
1125
- battery = build_widget(:battery)
1126
- add_service(battery)
1127
- [battery, true]
1864
+ service_with_created(:battery)
1128
1865
  end
1129
1866
 
1130
1867
  def ensure_share_service
1131
- share = services.find { |service| service.is_a?(Control) && service.type == "share" }
1132
- return share if share
1868
+ service(:share)
1869
+ end
1870
+
1871
+ def ensure_shared_preferences_service
1872
+ service(:shared_preferences)
1873
+ end
1874
+
1875
+ def invoke_shared_preferences(method_name, args: nil, timeout:, on_result:)
1876
+ shared_preferences = ensure_shared_preferences_service
1877
+ invoke(shared_preferences, method_name, args: args, timeout: timeout, on_result: on_result)
1878
+ end
1879
+
1880
+ def ensure_wakelock_service
1881
+ service(:wakelock)
1882
+ end
1883
+
1884
+ def invoke_wakelock(method_name, timeout:, on_result:)
1885
+ wakelock = ensure_wakelock_service
1886
+ invoke(wakelock, method_name, timeout: timeout, on_result: on_result)
1887
+ end
1888
+
1889
+ def ensure_flashlight_service
1890
+ service(:flashlight)
1891
+ end
1133
1892
 
1134
- share = build_widget(:share)
1135
- add_service(share)
1136
- share
1893
+ def invoke_flashlight(method_name, timeout:, on_result:)
1894
+ flashlight = ensure_flashlight_service
1895
+ invoke(flashlight, method_name, timeout: timeout, on_result: on_result)
1896
+ end
1897
+
1898
+ def ensure_screen_brightness_service
1899
+ service(:screen_brightness)
1900
+ end
1901
+
1902
+ def invoke_screen_brightness(method_name, args: nil, timeout:, on_result:)
1903
+ screen_brightness = ensure_screen_brightness_service
1904
+ invoke(screen_brightness, method_name, args: args, timeout: timeout, on_result: on_result)
1137
1905
  end
1138
1906
 
1139
1907
  def invoke_battery_method(method_name, timeout:, on_result:)
1140
- battery, created = ensure_battery_service
1141
- send_view_patch if created
1142
- sleep(0.05) if created
1908
+ battery, = ensure_battery_service
1143
1909
  invoke(
1144
1910
  battery,
1145
1911
  method_name,
1146
1912
  timeout: timeout,
1147
- on_result: lambda { |result, error|
1148
- message = error.to_s
1149
- if message.include?("inexistent control")
1150
- remove_service(battery)
1151
- fresh_battery, = ensure_battery_service
1152
- sleep(0.08)
1153
- invoke(
1154
- fresh_battery,
1155
- method_name,
1156
- timeout: timeout,
1157
- on_result: on_result
1158
- )
1159
- else
1160
- on_result&.call(result, error)
1161
- end
1162
- }
1913
+ on_result: on_result
1163
1914
  )
1164
1915
  rescue StandardError => e
1165
1916
  on_result&.call(nil, e.message)
1166
1917
  end
1167
1918
 
1168
1919
  def ensure_storage_paths_service
1169
- storage_paths = services.find do |service|
1170
- service.is_a?(Control) && %w[storage_paths storagepaths].include?(service.type.to_s)
1171
- end
1172
- return [storage_paths, false] if storage_paths
1173
-
1174
- storage_paths = build_widget(:storage_paths)
1175
- add_service(storage_paths)
1176
- [storage_paths, true]
1920
+ service_with_created(:storage_paths)
1177
1921
  end
1178
1922
 
1179
1923
  def invoke_storage_paths(method_name, timeout:, on_result:)
1180
- storage_paths, created = ensure_storage_paths_service
1181
- send_view_patch if created
1182
- sleep(0.05) if created
1924
+ storage_paths, = ensure_storage_paths_service
1183
1925
  invoke(
1184
1926
  storage_paths,
1185
1927
  method_name,
1186
1928
  timeout: timeout,
1187
- on_result: lambda { |result, error|
1188
- message = error.to_s
1189
- if message.include?("inexistent control")
1190
- remove_service(storage_paths)
1191
- fresh_storage_paths, = ensure_storage_paths_service
1192
- sleep(0.08)
1193
- invoke(
1194
- fresh_storage_paths,
1195
- method_name,
1196
- timeout: timeout,
1197
- on_result: on_result
1198
- )
1199
- else
1200
- on_result&.call(result, error)
1201
- end
1202
- }
1929
+ on_result: on_result
1203
1930
  )
1204
1931
  rescue StandardError => e
1205
1932
  on_result&.call(nil, e.message)
1206
1933
  end
1207
1934
 
1208
1935
  def invoke_file_picker(method_name, args, timeout:, on_result:)
1209
- picker = build_widget(:file_picker)
1210
- add_service(picker)
1936
+ picker = service(:file_picker)
1211
1937
  invoke(
1212
1938
  picker,
1213
1939
  method_name,
1214
1940
  args: args,
1215
1941
  timeout: timeout,
1216
- on_result: lambda { |result, error|
1217
- remove_service(picker)
1218
- on_result&.call(result, error)
1219
- }
1942
+ on_result: on_result
1220
1943
  )
1221
1944
  rescue StandardError => e
1222
- remove_service(picker) if picker
1223
1945
  on_result&.call(nil, e.message)
1224
1946
  end
1225
1947
  end