ruflet_core 0.0.12 → 0.0.13

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 +820 -145
  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 +35 -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 +240 -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 +53 -0
  124. data/lib/ruflet_ui/ruflet/ui/services/ruflet/securestorage_control.rb +89 -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,8 @@ module Ruflet
35
160
  @root_controls = []
36
161
  @views = []
37
162
  @dialogs = []
163
+ @services_container_mounted = false
164
+ @visual_service_controls = {}
38
165
  @page_event_handlers = {}
39
166
  @view_props = {}
40
167
  @page_props = { "route" => (client_details["route"] || "/") }
@@ -57,6 +184,10 @@ module Ruflet
57
184
  @invoke_waiters = {}
58
185
  @invoke_callbacks = {}
59
186
  @invoke_waiters_mutex = Mutex.new
187
+ @shared_preferences_proxy = SharedPreferencesService.new(self)
188
+ @wakelock_proxy = WakelockService.new(self)
189
+ @flashlight_proxy = FlashlightService.new(self)
190
+ @screen_brightness_proxy = ScreenBrightnessService.new(self)
60
191
  refresh_overlay_container!
61
192
  refresh_services_container!
62
193
  refresh_dialogs_container!
@@ -111,13 +242,14 @@ module Ruflet
111
242
  @view_props["bgcolor"] = normalize_value("bgcolor", value)
112
243
  end
113
244
 
114
- def add(*controls, appbar: nil, floating_action_button: nil, navigation_bar: nil, dialog: nil, snack_bar: nil, bottom_sheet: nil)
245
+ def add(*controls, appbar: nil, bottom_appbar: nil, floating_action_button: nil, navigation_bar: nil, dialog: nil, snack_bar: nil, bottom_sheet: nil)
115
246
  controls = controls.flatten
116
247
  visited = Set.new
117
248
  controls.each { |c| register_control_tree(c, visited) }
118
249
  @root_controls = controls
119
250
 
120
251
  @view_props["appbar"] = appbar if appbar
252
+ @view_props["bottom_appbar"] = bottom_appbar if bottom_appbar
121
253
  @view_props["floating_action_button"] = floating_action_button if floating_action_button
122
254
  @view_props["navigation_bar"] = navigation_bar if navigation_bar
123
255
  @dialog = dialog if dialog
@@ -148,6 +280,50 @@ module Ruflet
148
280
  self
149
281
  end
150
282
 
283
+ def shared_preferences(**props)
284
+ return service(:shared_preferences, **props) unless props.empty?
285
+
286
+ @shared_preferences_proxy
287
+ end
288
+
289
+ def wakelock(**props)
290
+ return service(:wakelock, **props) unless props.empty?
291
+
292
+ @wakelock_proxy
293
+ end
294
+
295
+ def flashlight(**props)
296
+ return service(:flashlight, **props) unless props.empty?
297
+
298
+ @flashlight_proxy
299
+ end
300
+
301
+ def screen_brightness(**props)
302
+ return service(:screen_brightness, **props) unless props.empty?
303
+
304
+ @screen_brightness_proxy
305
+ end
306
+
307
+ def audio(**props)
308
+ service(:audio, **props)
309
+ end
310
+
311
+ def audio_recorder(**props)
312
+ service(:audio_recorder, **props)
313
+ end
314
+
315
+ def browser_context_menu(**props)
316
+ service(:browser_context_menu, **props)
317
+ end
318
+
319
+ def window(**props)
320
+ service(:window, **props)
321
+ end
322
+
323
+ def tester(**props)
324
+ service(:tester, **props)
325
+ end
326
+
151
327
  def add_service(*value)
152
328
  @services_container.props["_services"] = services + value.flatten.compact
153
329
  refresh_services_container!
@@ -180,12 +356,25 @@ module Ruflet
180
356
  mapped_props = normalize_props(props || {})
181
357
  id = mapped_props.delete("id")
182
358
  normalized_type = type.to_s.downcase
359
+ compact_type = normalized_type.delete("_")
360
+
361
+ if visual_service_type?(normalized_type)
362
+ key = id ? "id:#{id}" : normalized_type
363
+ existing = @visual_service_controls[key]
364
+ return existing if existing
365
+
366
+ svc = Ruflet::UI::ControlFactory.build(type.to_s, id: id&.to_s, **mapped_props)
367
+ @visual_service_controls[key] = svc
368
+ return svc
369
+ end
183
370
 
184
371
  existing =
185
372
  if id
186
373
  services.find { |s| s.is_a?(Control) && s.id.to_s == id.to_s }
187
374
  else
188
- services.find { |s| s.is_a?(Control) && s.type.to_s.downcase == normalized_type }
375
+ services.find do |s|
376
+ s.is_a?(Control) && s.type.to_s.downcase.delete("_") == compact_type
377
+ end
189
378
  end
190
379
  return existing if existing
191
380
 
@@ -201,6 +390,18 @@ module Ruflet
201
390
  self
202
391
  end
203
392
 
393
+ def navigate(route, **query_params)
394
+ go(route, **query_params)
395
+ end
396
+
397
+ def push_route(route, **query_params)
398
+ go(route, **query_params)
399
+ end
400
+
401
+ def query
402
+ parse_query(route)
403
+ end
404
+
204
405
  def on_route_change=(handler)
205
406
  @page_event_handlers["route_change"] = handler
206
407
  end
@@ -224,10 +425,58 @@ module Ruflet
224
425
  @view_props["appbar"] = value
225
426
  end
226
427
 
428
+ def bottom_appbar=(value)
429
+ @view_props["bottom_appbar"] = value
430
+ end
431
+
432
+ def bottomappbar=(value)
433
+ self.bottom_appbar = value
434
+ end
435
+
227
436
  def floating_action_button=(value)
228
437
  @view_props["floating_action_button"] = value
229
438
  end
230
439
 
440
+ def drawer
441
+ @view_props["drawer"]
442
+ end
443
+
444
+ def drawer=(value)
445
+ @view_props["drawer"] = value
446
+ end
447
+
448
+ def end_drawer
449
+ @view_props["end_drawer"]
450
+ end
451
+
452
+ def end_drawer=(value)
453
+ @view_props["end_drawer"] = value
454
+ end
455
+
456
+ def show_drawer(timeout: 10, on_result: nil)
457
+ raise ArgumentError, "show_drawer requires drawer" unless drawer
458
+
459
+ invoke_current_view("show_drawer", timeout: timeout, on_result: on_result)
460
+ self
461
+ end
462
+
463
+ def close_drawer(timeout: 10, on_result: nil)
464
+ invoke_current_view("close_drawer", timeout: timeout, on_result: on_result)
465
+ self
466
+ end
467
+
468
+ def show_end_drawer(timeout: 10, on_result: nil)
469
+ raise ArgumentError, "show_end_drawer requires end_drawer" unless end_drawer
470
+
471
+ invoke_current_view("show_end_drawer", timeout: timeout, on_result: on_result)
472
+ self
473
+ end
474
+
475
+ def close_end_drawer(timeout: 10, on_result: nil)
476
+ invoke_current_view("close_end_drawer", timeout: timeout, on_result: on_result)
477
+ self
478
+ end
479
+
231
480
  def dialog = @dialog
232
481
 
233
482
  def dialog=(value)
@@ -328,6 +577,30 @@ module Ruflet
328
577
  invoke(url_launcher, "can_launch_url", args: { "url" => url }, timeout: timeout)
329
578
  end
330
579
 
580
+ def close_in_app_web_view(timeout: 10, on_result: nil)
581
+ url_launcher = ensure_url_launcher_service
582
+ invoke(url_launcher, "close_in_app_web_view", timeout: timeout, on_result: on_result)
583
+ end
584
+
585
+ def open_window(url, title: nil, width: nil, height: nil, timeout: 10, on_result: nil)
586
+ url_launcher = ensure_url_launcher_service
587
+ args = { "url" => url }
588
+ args["title"] = title unless title.nil?
589
+ args["width"] = width unless width.nil?
590
+ args["height"] = height unless height.nil?
591
+ invoke(url_launcher, "open_window", args: args, timeout: timeout, on_result: on_result)
592
+ end
593
+
594
+ def supports_launch_mode(mode, timeout: 10, on_result: nil)
595
+ url_launcher = ensure_url_launcher_service
596
+ invoke(url_launcher, "supports_launch_mode", args: { "mode" => mode }, timeout: timeout, on_result: on_result)
597
+ end
598
+
599
+ def supports_close_for_launch_mode(mode, timeout: 10, on_result: nil)
600
+ url_launcher = ensure_url_launcher_service
601
+ invoke(url_launcher, "supports_close_for_launch_mode", args: { "mode" => mode }, timeout: timeout, on_result: on_result)
602
+ end
603
+
331
604
  # File picker helpers: create an ephemeral service, invoke method, and dispose it.
332
605
  def pick_files(
333
606
  dialog_title: nil,
@@ -341,14 +614,14 @@ module Ruflet
341
614
  )
342
615
  invoke_file_picker(
343
616
  "pick_files",
344
- {
617
+ compact_service_args(
345
618
  "dialog_title" => dialog_title,
346
619
  "initial_directory" => initial_directory,
347
620
  "file_type" => file_type,
348
621
  "allowed_extensions" => allowed_extensions,
349
622
  "allow_multiple" => allow_multiple,
350
623
  "with_data" => with_data
351
- },
624
+ ),
352
625
  timeout: timeout,
353
626
  on_result: on_result
354
627
  )
@@ -366,14 +639,14 @@ module Ruflet
366
639
  )
367
640
  invoke_file_picker(
368
641
  "save_file",
369
- {
642
+ compact_service_args(
370
643
  "dialog_title" => dialog_title,
371
644
  "file_name" => file_name,
372
645
  "initial_directory" => initial_directory,
373
646
  "file_type" => file_type,
374
647
  "allowed_extensions" => allowed_extensions,
375
648
  "src_bytes" => src_bytes
376
- },
649
+ ),
377
650
  timeout: timeout,
378
651
  on_result: on_result
379
652
  )
@@ -382,18 +655,212 @@ module Ruflet
382
655
  def get_directory_path(dialog_title: nil, initial_directory: nil, timeout: nil, on_result: nil)
383
656
  invoke_file_picker(
384
657
  "get_directory_path",
385
- {
658
+ compact_service_args(
386
659
  "dialog_title" => dialog_title,
387
660
  "initial_directory" => initial_directory
388
- },
661
+ ),
662
+ timeout: timeout,
663
+ on_result: on_result
664
+ )
665
+ end
666
+
667
+ def upload(files, timeout: nil, on_result: nil)
668
+ invoke_file_picker(
669
+ "upload",
670
+ { "files" => Array(files).map { |file| normalize_service_value(file) } },
671
+ timeout: timeout,
672
+ on_result: on_result
673
+ )
674
+ end
675
+
676
+ def upload_files(files, timeout: nil, on_result: nil)
677
+ upload(files, timeout: timeout, on_result: on_result)
678
+ end
679
+
680
+ def disable_browser_context_menu(timeout: 10, on_result: nil)
681
+ invoke_browser_context_menu("disable_menu", timeout: timeout, on_result: on_result)
682
+ end
683
+
684
+ def enable_browser_context_menu(timeout: 10, on_result: nil)
685
+ invoke_browser_context_menu("enable_menu", timeout: timeout, on_result: on_result)
686
+ end
687
+
688
+ def wait_until_ready_to_show(timeout: 10, on_result: nil)
689
+ invoke_window("wait_until_ready_to_show", timeout: timeout, on_result: on_result)
690
+ end
691
+
692
+ def window_to_front(timeout: 10, on_result: nil)
693
+ invoke_window("to_front", timeout: timeout, on_result: on_result)
694
+ end
695
+
696
+ def center_window(timeout: 10, on_result: nil)
697
+ invoke_window("center", timeout: timeout, on_result: on_result)
698
+ end
699
+
700
+ def close_window(timeout: 10, on_result: nil)
701
+ invoke_window("close", timeout: timeout, on_result: on_result)
702
+ end
703
+
704
+ def destroy_window(timeout: 10, on_result: nil)
705
+ invoke_window("destroy", timeout: timeout, on_result: on_result)
706
+ end
707
+
708
+ def start_window_dragging(timeout: 10, on_result: nil)
709
+ invoke_window("start_dragging", timeout: timeout, on_result: on_result)
710
+ end
711
+
712
+ def start_window_resizing(edge, timeout: 10, on_result: nil)
713
+ invoke_window(
714
+ "start_resizing",
715
+ args: { "edge" => normalize_service_value(edge) },
716
+ timeout: timeout,
717
+ on_result: on_result
718
+ )
719
+ end
720
+
721
+ def tester_pump(options = nil, duration: nil, timeout: 10, on_result: nil)
722
+ duration = options[:duration] || options["duration"] if options.is_a?(Hash) && duration.nil?
723
+ invoke_tester("pump", args: compact_service_args("duration" => duration), timeout: timeout, on_result: on_result)
724
+ end
725
+
726
+ def tester_pump_and_settle(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_and_settle", args: compact_service_args("duration" => duration), timeout: timeout, on_result: on_result)
729
+ end
730
+
731
+ def find_by_text(text, timeout: 10, on_result: nil)
732
+ invoke_tester("find_by_text", args: { "text" => text }, timeout: timeout, on_result: on_result)
733
+ end
734
+
735
+ def find_by_text_containing(pattern, timeout: 10, on_result: nil)
736
+ invoke_tester("find_by_text_containing", args: { "pattern" => pattern }, timeout: timeout, on_result: on_result)
737
+ end
738
+
739
+ def find_by_key(key, timeout: 10, on_result: nil)
740
+ invoke_tester("find_by_key", args: { "key" => key }, timeout: timeout, on_result: on_result)
741
+ end
742
+
743
+ def find_by_tooltip(value, timeout: 10, on_result: nil)
744
+ invoke_tester("find_by_tooltip", args: { "value" => value }, timeout: timeout, on_result: on_result)
745
+ end
746
+
747
+ def find_by_icon(icon, timeout: 10, on_result: nil)
748
+ invoke_tester("find_by_icon", args: { "icon" => normalize_service_value(icon) }, timeout: timeout, on_result: on_result)
749
+ end
750
+
751
+ def take_screenshot(name, timeout: 10, on_result: nil)
752
+ invoke_tester("take_screenshot", args: { "name" => name }, timeout: timeout, on_result: on_result)
753
+ end
754
+
755
+ def tap(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
756
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
757
+ invoke_tester_finder("tap", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
758
+ end
759
+
760
+ def mouse_click(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("mouse_click", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
763
+ end
764
+
765
+ def mouse_double_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_double_click", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
768
+ end
769
+
770
+ def right_mouse_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("right_mouse_click", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
773
+ end
774
+
775
+ def tap_at(offset = nil, timeout: 10, on_result: nil)
776
+ invoke_tester_at("tap_at", offset, timeout: timeout, on_result: on_result)
777
+ end
778
+
779
+ def mouse_click_at(offset = nil, timeout: 10, on_result: nil)
780
+ invoke_tester_at("mouse_click_at", offset, timeout: timeout, on_result: on_result)
781
+ end
782
+
783
+ def mouse_double_click_at(offset = nil, timeout: 10, on_result: nil)
784
+ invoke_tester_at("mouse_double_click_at", offset, timeout: timeout, on_result: on_result)
785
+ end
786
+
787
+ def right_mouse_click_at(offset = nil, timeout: 10, on_result: nil)
788
+ invoke_tester_at("right_mouse_click_at", offset, timeout: timeout, on_result: on_result)
789
+ end
790
+
791
+ def drag(finder_id, offset, finder_index: nil, timeout: 10, on_result: nil)
792
+ invoke_tester(
793
+ "drag",
794
+ args: compact_service_args(
795
+ "finder_id" => finder_id,
796
+ "finder_index" => finder_index,
797
+ "offset" => offset
798
+ ),
389
799
  timeout: timeout,
390
800
  on_result: on_result
391
801
  )
392
802
  end
393
803
 
804
+ def drag_from(start, offset, timeout: 10, on_result: nil)
805
+ invoke_tester(
806
+ "drag_from",
807
+ args: compact_service_args("start" => start, "offset" => offset),
808
+ timeout: timeout,
809
+ on_result: on_result
810
+ )
811
+ end
812
+
813
+ def long_press(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
814
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
815
+ invoke_tester_finder("long_press", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
816
+ end
817
+
818
+ def enter_text(finder_id, text, 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(
821
+ "enter_text",
822
+ args: compact_service_args(
823
+ "finder_id" => finder_id,
824
+ "finder_index" => finder_index,
825
+ "text" => text
826
+ ),
827
+ timeout: timeout,
828
+ on_result: on_result
829
+ )
830
+ end
831
+
832
+ def mouse_hover(finder_id = nil, options = nil, finder_index: nil, timeout: 10, on_result: nil)
833
+ finder_index = options[:finder_index] || options["finder_index"] if options.is_a?(Hash) && finder_index.nil?
834
+ invoke_tester_finder("mouse_hover", finder_id, finder_index: finder_index, timeout: timeout, on_result: on_result)
835
+ end
836
+
837
+ def tester_teardown(timeout: 10, on_result: nil)
838
+ invoke_tester("teardown", timeout: timeout, on_result: on_result)
839
+ end
840
+
841
+ def heavy_impact(timeout: 10, on_result: nil)
842
+ invoke_haptic_feedback("heavy_impact", timeout: timeout, on_result: on_result)
843
+ end
844
+
845
+ def medium_impact(timeout: 10, on_result: nil)
846
+ invoke_haptic_feedback("medium_impact", timeout: timeout, on_result: on_result)
847
+ end
848
+
849
+ def light_impact(timeout: 10, on_result: nil)
850
+ invoke_haptic_feedback("light_impact", timeout: timeout, on_result: on_result)
851
+ end
852
+
853
+ def selection_click(timeout: 10, on_result: nil)
854
+ invoke_haptic_feedback("selection_click", timeout: timeout, on_result: on_result)
855
+ end
856
+
857
+ def vibrate(timeout: 10, on_result: nil)
858
+ invoke_haptic_feedback("vibrate", timeout: timeout, on_result: on_result)
859
+ end
860
+
394
861
  def set_clipboard(value, timeout: nil, on_result: nil)
395
862
  invoke_clipboard_method(
396
- "set_data",
863
+ "set",
397
864
  args: { "data" => value.to_s },
398
865
  timeout: timeout,
399
866
  on_result: on_result
@@ -401,7 +868,7 @@ module Ruflet
401
868
  end
402
869
 
403
870
  def get_clipboard(timeout: nil, on_result: nil)
404
- invoke_clipboard_method("get_data", timeout: timeout, on_result: on_result)
871
+ invoke_clipboard_method("get", timeout: timeout, on_result: on_result)
405
872
  end
406
873
 
407
874
  def set_clipboard_files(files, timeout: nil, on_result: nil)
@@ -442,10 +909,94 @@ module Ruflet
442
909
  invoke_battery_method("get_battery_state", timeout: timeout, on_result: on_result)
443
910
  end
444
911
 
445
- def battery_save_mode?(timeout: nil, on_result: nil)
912
+ def is_in_battery_save_mode(timeout: nil, on_result: nil)
446
913
  invoke_battery_method("is_in_battery_save_mode", timeout: timeout, on_result: on_result)
447
914
  end
448
915
 
916
+ def battery_save_mode?(timeout: nil, on_result: nil)
917
+ is_in_battery_save_mode(timeout: timeout, on_result: on_result)
918
+ end
919
+
920
+ def accelerometer(**props)
921
+ service(:accelerometer, **props)
922
+ end
923
+
924
+ def gyroscope(**props)
925
+ service(:gyroscope, **props)
926
+ end
927
+
928
+ def user_accelerometer(**props)
929
+ service(:user_accelerometer, **props)
930
+ end
931
+
932
+ def magnetometer(**props)
933
+ service(:magnetometer, **props)
934
+ end
935
+
936
+ def barometer(**props)
937
+ service(:barometer, **props)
938
+ end
939
+
940
+ def shake_detector(**props)
941
+ service(:shake_detector, **props)
942
+ end
943
+
944
+ def semantics_service(**props)
945
+ service(:semantics_service, **props)
946
+ end
947
+
948
+ def screenshot(**props)
949
+ service(:screenshot, **props)
950
+ end
951
+
952
+ def battery(**props)
953
+ service(:battery, **props)
954
+ end
955
+
956
+ def connectivity(**props)
957
+ service(:connectivity, **props)
958
+ end
959
+
960
+ def clipboard(**props)
961
+ service(:clipboard, **props)
962
+ end
963
+
964
+ def file_picker(**props)
965
+ service(:file_picker, **props)
966
+ end
967
+
968
+ def url_launcher(**props)
969
+ service(:url_launcher, **props)
970
+ end
971
+
972
+ def storage_paths(**props)
973
+ service(:storage_paths, **props)
974
+ end
975
+
976
+ def share(**props)
977
+ service(:share, **props)
978
+ end
979
+
980
+ def camera(**props)
981
+ service(:camera, **props)
982
+ end
983
+
984
+ def haptic_feedback(**props)
985
+ service(:haptic_feedback, **props)
986
+ end
987
+
988
+ def geolocator(**props)
989
+ service(:geolocator, **props)
990
+ end
991
+
992
+ def permission_handler(**props)
993
+ service(:permission_handler, **props)
994
+ end
995
+
996
+ def secure_storage(**props)
997
+ service(:secure_storage, **props)
998
+ end
999
+
449
1000
  def get_application_cache_directory(timeout: nil, on_result: nil)
450
1001
  invoke_storage_paths("get_application_cache_directory", timeout: timeout, on_result: on_result)
451
1002
  end
@@ -487,7 +1038,7 @@ module Ruflet
487
1038
  end
488
1039
 
489
1040
  def share_text(
490
- text:,
1041
+ text = nil,
491
1042
  title: nil,
492
1043
  subject: nil,
493
1044
  preview_thumbnail: nil,
@@ -502,7 +1053,7 @@ module Ruflet
502
1053
  invoke(
503
1054
  share,
504
1055
  "share_text",
505
- args: {
1056
+ args: compact_service_args(
506
1057
  "text" => text,
507
1058
  "title" => title,
508
1059
  "subject" => subject,
@@ -511,14 +1062,14 @@ module Ruflet
511
1062
  "download_fallback_enabled" => download_fallback_enabled,
512
1063
  "mail_to_fallback_enabled" => mail_to_fallback_enabled,
513
1064
  "excluded_cupertino_activities" => excluded_cupertino_activities
514
- },
1065
+ ),
515
1066
  timeout: timeout,
516
1067
  on_result: on_result
517
1068
  )
518
1069
  end
519
1070
 
520
1071
  def share_uri(
521
- uri:,
1072
+ uri = nil,
522
1073
  share_position_origin: nil,
523
1074
  excluded_cupertino_activities: nil,
524
1075
  timeout: nil,
@@ -528,18 +1079,18 @@ module Ruflet
528
1079
  invoke(
529
1080
  share,
530
1081
  "share_uri",
531
- args: {
1082
+ args: compact_service_args(
532
1083
  "uri" => uri,
533
1084
  "share_position_origin" => share_position_origin,
534
1085
  "excluded_cupertino_activities" => excluded_cupertino_activities
535
- },
1086
+ ),
536
1087
  timeout: timeout,
537
1088
  on_result: on_result
538
1089
  )
539
1090
  end
540
1091
 
541
1092
  def share_files(
542
- files:,
1093
+ files = nil,
543
1094
  text: nil,
544
1095
  title: nil,
545
1096
  subject: nil,
@@ -555,17 +1106,17 @@ module Ruflet
555
1106
  invoke(
556
1107
  share,
557
1108
  "share_files",
558
- args: {
559
- "files" => files,
1109
+ args: compact_service_args(
1110
+ "files" => normalize_share_files(files),
560
1111
  "text" => text,
561
1112
  "title" => title,
562
1113
  "subject" => subject,
563
- "preview_thumbnail" => preview_thumbnail,
1114
+ "preview_thumbnail" => normalize_share_file(preview_thumbnail),
564
1115
  "share_position_origin" => share_position_origin,
565
1116
  "download_fallback_enabled" => download_fallback_enabled,
566
1117
  "mail_to_fallback_enabled" => mail_to_fallback_enabled,
567
1118
  "excluded_cupertino_activities" => excluded_cupertino_activities
568
- },
1119
+ ),
569
1120
  timeout: timeout,
570
1121
  on_result: on_result
571
1122
  )
@@ -675,6 +1226,7 @@ module Ruflet
675
1226
  return unless control
676
1227
 
677
1228
  event = Event.new(name: name, target: target, raw_data: data, page: self, control: control)
1229
+ apply_event_value_to_control(control, event) if %w[change select select_change].include?(name.to_s)
678
1230
  control.emit(name, event)
679
1231
 
680
1232
  if name.to_s == "dismiss" && remove_dialog_tracking(control)
@@ -754,10 +1306,56 @@ module Ruflet
754
1306
 
755
1307
  def build_widget(type, **props, &block) = WidgetBuilder.new.control(type, **props, &block)
756
1308
 
1309
+ def compact_service_args(hash)
1310
+ hash.each_with_object({}) do |(key, value), result|
1311
+ result[key] = normalize_service_value(value) unless value.nil?
1312
+ end
1313
+ end
1314
+
1315
+ def normalize_service_value(value)
1316
+ case value
1317
+ when Array
1318
+ value.map { |item| normalize_service_value(item) }
1319
+ when Hash
1320
+ value.transform_keys(&:to_s).each_with_object({}) do |(key, item), result|
1321
+ next if item.nil?
1322
+
1323
+ result[key] = key == "data" && byte_array?(item) ? item.pack("C*").b : normalize_service_value(item)
1324
+ end
1325
+ else
1326
+ value
1327
+ end
1328
+ end
1329
+
1330
+ def normalize_share_files(files)
1331
+ return nil if files.nil?
1332
+
1333
+ Array(files).map { |file| normalize_share_file(file) }
1334
+ end
1335
+
1336
+ def normalize_share_file(file)
1337
+ case file
1338
+ when nil
1339
+ nil
1340
+ when String
1341
+ { "path" => file }
1342
+ else
1343
+ normalize_service_value(file)
1344
+ end
1345
+ end
1346
+
1347
+ def byte_array?(value)
1348
+ value.is_a?(Array) && value.all? { |item| item.is_a?(Integer) && item.between?(0, 255) }
1349
+ end
1350
+
757
1351
  def widget_helper_method?(name)
758
1352
  WIDGET_HELPER_METHODS.include?(name.to_s)
759
1353
  end
760
1354
 
1355
+ def visual_service_type?(type)
1356
+ type.to_s.delete("_") == "camera"
1357
+ end
1358
+
761
1359
  def text_maps_to_content?(control, patch)
762
1360
  patch.key?("text") && control.type.end_with?("button")
763
1361
  end
@@ -785,6 +1383,7 @@ module Ruflet
785
1383
  *page_patch_ops
786
1384
  ]
787
1385
  })
1386
+ @services_container_mounted = true if @services_container.wire_id
788
1387
  end
789
1388
 
790
1389
  def register_control_tree(control, visited = Set.new)
@@ -895,6 +1494,15 @@ module Ruflet
895
1494
  "#{base}#{separator}#{query}"
896
1495
  end
897
1496
 
1497
+ def parse_query(route_value)
1498
+ query_string = route_value.to_s.split("?", 2)[1].to_s
1499
+ return {} if query_string.empty?
1500
+
1501
+ CGI.parse(query_string).each_with_object({}) do |(key, values), result|
1502
+ result[key] = values.size == 1 ? values.first : values
1503
+ end
1504
+ end
1505
+
898
1506
  def extract_route(data)
899
1507
  case data
900
1508
  when String
@@ -910,10 +1518,46 @@ module Ruflet
910
1518
  handler = @page_event_handlers[name.to_s.sub(/\Aon_/, "")]
911
1519
  return unless handler.respond_to?(:call)
912
1520
 
913
- event = Event.new(name: name.to_s, target: 1, raw_data: data, page: self, control: nil)
1521
+ event = Ruflet::Event.new(name: name.to_s, target: 1, raw_data: data, page: self, control: nil)
914
1522
  handler.call(event)
915
1523
  end
916
1524
 
1525
+ def apply_event_value_to_control(control, event)
1526
+ return unless event.typed_data && event.typed_data.respond_to?(:value)
1527
+
1528
+ value = event.typed_data.value
1529
+ if control.props.key?("start_value") && control.props.key?("end_value")
1530
+ raw = event.typed_data.respond_to?(:raw) ? event.typed_data.raw : event.data
1531
+ range_value = value.is_a?(Hash) ? value : raw
1532
+ if range_value.is_a?(Hash)
1533
+ start_value = range_value["start_value"] || range_value[:start_value]
1534
+ end_value = range_value["end_value"] || range_value[:end_value]
1535
+ control.props["start_value"] = start_value unless start_value.nil?
1536
+ control.props["end_value"] = end_value unless end_value.nil?
1537
+ return
1538
+ end
1539
+ end
1540
+
1541
+ return if value.nil?
1542
+ return if control.type == "selectionarea"
1543
+
1544
+ prop_name =
1545
+ if event.name == "select"
1546
+ control.props.key?("value") ? "value" : "selected"
1547
+ elsif event.name == "select_change" && control.props.key?("selected")
1548
+ "selected"
1549
+ elsif control.props.key?("expanded")
1550
+ "expanded"
1551
+ elsif control.props.key?("selected")
1552
+ "selected"
1553
+ elsif control.props.key?("selected_index")
1554
+ "selected_index"
1555
+ else
1556
+ "value"
1557
+ end
1558
+ control.props[prop_name] = value
1559
+ end
1560
+
917
1561
  def page_control_target?(control_or_id)
918
1562
  control_or_id == 1 || control_or_id.to_s == "1" || control_or_id.to_s == "page"
919
1563
  end
@@ -927,7 +1571,7 @@ module Ruflet
927
1571
  when Array
928
1572
  value.map { |v| serialize_patch_value(v) }
929
1573
  when Hash
930
- value.transform_values { |v| serialize_patch_value(v) }
1574
+ value.each_with_object({}) { |(k, v), result| result[k.to_s] = serialize_patch_value(v) }
931
1575
  else
932
1576
  value
933
1577
  end
@@ -1026,200 +1670,231 @@ module Ruflet
1026
1670
  # same IDs and detach service invoke listeners on the Flutter side.
1027
1671
  next nil if k == "_overlay" && @overlay_container.wire_id
1028
1672
  next nil if k == "_dialogs" && @dialogs_container.wire_id
1673
+ next nil if k == "_services" && @services_container_mounted
1029
1674
 
1030
1675
  [0, 0, k, serialize_patch_value(v)]
1031
1676
  end
1032
1677
  end
1033
1678
 
1034
- def ensure_clipboard_service
1035
- clipboard = services.find { |service| service.is_a?(Control) && service.type == "clipboard" }
1036
- return [clipboard, false] if clipboard
1679
+ def service_by_type(type)
1680
+ compact_type = type.to_s.downcase.delete("_")
1681
+ services.find do |service|
1682
+ service.is_a?(Control) && service.type.to_s.downcase.delete("_") == compact_type
1683
+ end
1684
+ end
1685
+
1686
+ def service_with_created(type)
1687
+ existing = service_by_type(type)
1688
+ return [existing, false] if existing
1689
+
1690
+ [service(type), true]
1691
+ end
1037
1692
 
1038
- clipboard = build_widget(:clipboard)
1039
- add_service(clipboard)
1040
- [clipboard, true]
1693
+ def ensure_clipboard_service
1694
+ service_with_created(:clipboard)
1041
1695
  end
1042
1696
 
1043
1697
  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
1698
+ clipboard, = ensure_clipboard_service
1047
1699
  invoke(
1048
1700
  clipboard,
1049
1701
  method_name,
1050
1702
  args: args,
1051
1703
  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
- }
1704
+ on_result: on_result
1069
1705
  )
1070
1706
  rescue StandardError => e
1071
1707
  on_result&.call(nil, e.message)
1072
1708
  end
1073
1709
 
1074
1710
  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
1711
+ service(:url_launcher)
1712
+ end
1077
1713
 
1078
- url_launcher = build_widget(:url_launcher)
1079
- add_service(url_launcher)
1080
- url_launcher
1714
+ def ensure_browser_context_menu_service
1715
+ service(:browser_context_menu)
1081
1716
  end
1082
1717
 
1083
- def ensure_connectivity_service
1084
- connectivity = services.find { |service| service.is_a?(Control) && service.type == "connectivity" }
1085
- return [connectivity, false] if connectivity
1718
+ def invoke_browser_context_menu(method_name, timeout:, on_result:)
1719
+ browser_context_menu = ensure_browser_context_menu_service
1720
+ invoke(browser_context_menu, method_name, timeout: timeout, on_result: on_result)
1721
+ end
1722
+
1723
+ def ensure_window_service
1724
+ service(:window)
1725
+ end
1726
+
1727
+ def invoke_window(method_name, args: nil, timeout:, on_result:)
1728
+ window = ensure_window_service
1729
+ invoke(window, method_name, args: args, timeout: timeout, on_result: on_result)
1730
+ end
1731
+
1732
+ def ensure_tester_service
1733
+ service(:tester)
1734
+ end
1735
+
1736
+ def invoke_tester(method_name, args: nil, timeout:, on_result:)
1737
+ tester = ensure_tester_service
1738
+ invoke(tester, method_name, args: args, timeout: timeout, on_result: on_result)
1739
+ end
1740
+
1741
+ def invoke_tester_finder(method_name, finder_id, finder_index:, timeout:, on_result:)
1742
+ invoke_tester(
1743
+ method_name,
1744
+ args: compact_service_args("finder_id" => finder_id, "finder_index" => finder_index),
1745
+ timeout: timeout,
1746
+ on_result: on_result
1747
+ )
1748
+ end
1749
+
1750
+ def invoke_tester_at(method_name, offset, timeout:, on_result:)
1751
+ invoke_tester(
1752
+ method_name,
1753
+ args: compact_service_args("offset" => offset),
1754
+ timeout: timeout,
1755
+ on_result: on_result
1756
+ )
1757
+ end
1086
1758
 
1087
- connectivity = build_widget(:connectivity)
1088
- add_service(connectivity)
1089
- [connectivity, true]
1759
+ def ensure_haptic_feedback_service
1760
+ service(:haptic_feedback)
1761
+ end
1762
+
1763
+ def invoke_haptic_feedback(method_name, timeout:, on_result:)
1764
+ haptic_feedback = ensure_haptic_feedback_service
1765
+ invoke(haptic_feedback, method_name, timeout: timeout, on_result: on_result)
1766
+ end
1767
+
1768
+ def invoke_current_view(method_name, timeout:, on_result:)
1769
+ target_id = @views.last&.wire_id || @view_id
1770
+ invoke_control_id(target_id, method_name, timeout: timeout, on_result: on_result)
1771
+ end
1772
+
1773
+ def invoke_control_id(control_id, method_name, args: nil, timeout: 10, on_result: nil)
1774
+ call_id = "call_#{Ruflet::Control.generate_id}"
1775
+ if on_result.respond_to?(:call)
1776
+ @invoke_waiters_mutex.synchronize { @invoke_callbacks[call_id] = on_result }
1777
+ unless timeout.nil?
1778
+ Thread.new(call_id, timeout.to_f) do |pending_call_id, invoke_timeout|
1779
+ sleep([invoke_timeout, 0.0].max + 0.1)
1780
+ callback = @invoke_waiters_mutex.synchronize { @invoke_callbacks.delete(pending_call_id) }
1781
+ callback&.call(nil, "execution expired")
1782
+ rescue StandardError => e
1783
+ Kernel.warn("invoke timeout callback error: #{e.class}: #{e.message}")
1784
+ end
1785
+ end
1786
+ end
1787
+
1788
+ payload = {
1789
+ "control_id" => control_id,
1790
+ "call_id" => call_id,
1791
+ "name" => method_name.to_s,
1792
+ "args" => args
1793
+ }
1794
+ payload["timeout"] = timeout unless timeout.nil?
1795
+ send_message(Protocol::ACTIONS[:invoke_control_method], payload)
1796
+
1797
+ call_id
1798
+ end
1799
+
1800
+ def ensure_connectivity_service
1801
+ service_with_created(:connectivity)
1090
1802
  end
1091
1803
 
1092
1804
  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
1805
+ connectivity, = ensure_connectivity_service
1096
1806
  invoke(
1097
1807
  connectivity,
1098
1808
  method_name,
1099
1809
  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
- }
1810
+ on_result: on_result
1116
1811
  )
1117
1812
  rescue StandardError => e
1118
1813
  on_result&.call(nil, e.message)
1119
1814
  end
1120
1815
 
1121
1816
  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]
1817
+ service_with_created(:battery)
1128
1818
  end
1129
1819
 
1130
1820
  def ensure_share_service
1131
- share = services.find { |service| service.is_a?(Control) && service.type == "share" }
1132
- return share if share
1821
+ service(:share)
1822
+ end
1823
+
1824
+ def ensure_shared_preferences_service
1825
+ service(:shared_preferences)
1826
+ end
1827
+
1828
+ def invoke_shared_preferences(method_name, args: nil, timeout:, on_result:)
1829
+ shared_preferences = ensure_shared_preferences_service
1830
+ invoke(shared_preferences, method_name, args: args, timeout: timeout, on_result: on_result)
1831
+ end
1832
+
1833
+ def ensure_wakelock_service
1834
+ service(:wakelock)
1835
+ end
1836
+
1837
+ def invoke_wakelock(method_name, timeout:, on_result:)
1838
+ wakelock = ensure_wakelock_service
1839
+ invoke(wakelock, method_name, timeout: timeout, on_result: on_result)
1840
+ end
1841
+
1842
+ def ensure_flashlight_service
1843
+ service(:flashlight)
1844
+ end
1845
+
1846
+ def invoke_flashlight(method_name, timeout:, on_result:)
1847
+ flashlight = ensure_flashlight_service
1848
+ invoke(flashlight, method_name, timeout: timeout, on_result: on_result)
1849
+ end
1850
+
1851
+ def ensure_screen_brightness_service
1852
+ service(:screen_brightness)
1853
+ end
1133
1854
 
1134
- share = build_widget(:share)
1135
- add_service(share)
1136
- share
1855
+ def invoke_screen_brightness(method_name, args: nil, timeout:, on_result:)
1856
+ screen_brightness = ensure_screen_brightness_service
1857
+ invoke(screen_brightness, method_name, args: args, timeout: timeout, on_result: on_result)
1137
1858
  end
1138
1859
 
1139
1860
  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
1861
+ battery, = ensure_battery_service
1143
1862
  invoke(
1144
1863
  battery,
1145
1864
  method_name,
1146
1865
  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
- }
1866
+ on_result: on_result
1163
1867
  )
1164
1868
  rescue StandardError => e
1165
1869
  on_result&.call(nil, e.message)
1166
1870
  end
1167
1871
 
1168
1872
  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]
1873
+ service_with_created(:storage_paths)
1177
1874
  end
1178
1875
 
1179
1876
  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
1877
+ storage_paths, = ensure_storage_paths_service
1183
1878
  invoke(
1184
1879
  storage_paths,
1185
1880
  method_name,
1186
1881
  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
- }
1882
+ on_result: on_result
1203
1883
  )
1204
1884
  rescue StandardError => e
1205
1885
  on_result&.call(nil, e.message)
1206
1886
  end
1207
1887
 
1208
1888
  def invoke_file_picker(method_name, args, timeout:, on_result:)
1209
- picker = build_widget(:file_picker)
1210
- add_service(picker)
1889
+ picker = service(:file_picker)
1211
1890
  invoke(
1212
1891
  picker,
1213
1892
  method_name,
1214
1893
  args: args,
1215
1894
  timeout: timeout,
1216
- on_result: lambda { |result, error|
1217
- remove_service(picker)
1218
- on_result&.call(result, error)
1219
- }
1895
+ on_result: on_result
1220
1896
  )
1221
1897
  rescue StandardError => e
1222
- remove_service(picker) if picker
1223
1898
  on_result&.call(nil, e.message)
1224
1899
  end
1225
1900
  end