ruflet 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 959ea77a03c912a7126700df435c3d3a2eaf906cd032cd216967ff7406840d3c
4
- data.tar.gz: e47d421f67d39b245c4c6680ae3baa041639abbe92ba716db4d1c5c77efee7a5
3
+ metadata.gz: 8f114a59fe4013e5313b51fc7b8b3373dfa7120e57422bc33abb2e224f9724d6
4
+ data.tar.gz: fe0b491c3eff667a72c31521209953a2f7ba8249c1d16f6ec9ac08243099f850
5
5
  SHA512:
6
- metadata.gz: 5694f803d70cfeea5f9c86ffbbc4ae175332b6e7a7aee60bcc34c4ee3033c18c82b9a615f877bba4d082eb6f64a2ad763a72e60e205729adc262ff4a606a0248
7
- data.tar.gz: ce91d9909ab65a24a87ab9f3c9be27e2996bcd5d8974852516e88db87522aeb668532373dd027d31a660798493bf97bb44e0aabcb134a656f203944a3d552820
6
+ metadata.gz: cbf26f1c4752d3fdd13600ff8b0bd305e3e669165c9cda9d094d4e4687f801adcb1df015230a509c016f8305f195c32459af74eeacdc8829475f62ac8c7af591
7
+ data.tar.gz: a59f249b3b71255a4b499e8e87c69216c3e61d50c974a786cc1d926b7d502b500effe4161b5c2d8c3c384078ead1cfe1194106c872ca4a17bdafb05f180e36f7
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Ruflet
7
+ module ManifestCompiler
8
+ module_function
9
+
10
+ def compile_app(app, route: "/")
11
+ messages = []
12
+ sender = lambda do |action, payload|
13
+ messages << { "action" => action, "payload" => payload }
14
+ end
15
+
16
+ page = Ruflet::Page.new(
17
+ session_id: "manifest",
18
+ client_details: { "route" => route.to_s.empty? ? "/" : route.to_s },
19
+ sender: sender
20
+ )
21
+
22
+ app.view(page)
23
+ page.update
24
+
25
+ compacted = compact_messages(messages)
26
+
27
+ {
28
+ "schema" => "ruflet_manifest/v1",
29
+ "generated_at" => Time.now.utc.iso8601,
30
+ "route" => page.route || "/",
31
+ "messages" => compacted
32
+ }
33
+ end
34
+
35
+ def write_file(path, manifest)
36
+ File.write(path, JSON.pretty_generate(manifest))
37
+ path
38
+ end
39
+
40
+ def read_file(path)
41
+ JSON.parse(File.read(path.to_s))
42
+ end
43
+
44
+ def compact_messages(messages)
45
+ full_patch_index = nil
46
+ messages.each_with_index do |message, idx|
47
+ next unless message["action"] == Ruflet::Protocol::ACTIONS[:patch_control]
48
+
49
+ payload = message["payload"] || {}
50
+ next unless payload["id"] == 1
51
+
52
+ patch = payload["patch"]
53
+ next unless patch.is_a?(Array) && patch.any? { |op| op.is_a?(Array) && op[2] == "views" }
54
+
55
+ full_patch_index = idx
56
+ end
57
+ return messages if full_patch_index.nil?
58
+
59
+ [messages[full_patch_index]]
60
+ end
61
+ end
62
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.3" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.5" unless const_defined?(:VERSION)
5
5
  end
data/lib/ruflet.rb CHANGED
@@ -11,12 +11,6 @@ module Ruflet
11
11
  module_function
12
12
 
13
13
  def run(entrypoint = nil, host: "0.0.0.0", port: 8550, &block)
14
- begin
15
- require "ruflet_server"
16
- rescue LoadError => e
17
- raise LoadError, "Ruflet.run requires the 'ruflet_server' gem. Add it to your Gemfile.", e.backtrace
18
- end
19
-
20
14
  callback = entrypoint || block
21
15
  raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
22
16
 
@@ -26,6 +20,12 @@ module Ruflet
26
20
  return result unless result == :pass
27
21
  end
28
22
 
23
+ begin
24
+ require "ruflet_server"
25
+ rescue LoadError => e
26
+ raise LoadError, "Ruflet.run requires the 'ruflet_server' gem unless a run interceptor handles execution.", e.backtrace
27
+ end
28
+
29
29
  Server.new(host: host, port: port) do |page|
30
30
  callback.call(page)
31
31
  end.start
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require_relative "../../ruflet/manifest_compiler"
2
3
 
3
4
  module Ruflet
4
5
  class App
@@ -8,6 +9,16 @@ module Ruflet
8
9
  end
9
10
 
10
11
  def run
12
+ manifest_out = ENV["RUFLET_MANIFEST_OUT"].to_s
13
+ unless manifest_out.empty?
14
+ route = ENV["RUFLET_MANIFEST_ROUTE"].to_s
15
+ route = "/" if route.empty?
16
+ manifest = Ruflet::ManifestCompiler.compile_app(self, route: route)
17
+ Ruflet::ManifestCompiler.write_file(manifest_out, manifest)
18
+ puts manifest_out
19
+ return manifest_out
20
+ end
21
+
11
22
  Ruflet.run(host: @host, port: @port) do |page|
12
23
  view(page)
13
24
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Ruflet
4
4
  module Colors
5
- module_function
6
-
7
5
  SEMANTIC_COLORS = {
8
6
  PRIMARY: "primary",
9
7
  ON_PRIMARY: "onprimary",
@@ -127,14 +125,32 @@ module Ruflet
127
125
  end
128
126
 
129
127
  def all_values
130
- @all_values ||= constants(false)
131
- .map { |c| const_get(c) }
132
- .select { |v| v.is_a?(String) }
133
- .uniq
134
- .freeze
128
+ return @all_values if @all_values
129
+
130
+ values = []
131
+ SEMANTIC_COLORS.each_value { |v| values << v }
132
+ FIXED_COLORS.each_value { |v| values << v }
133
+
134
+ BASE_PRIMARY.each do |base|
135
+ values << base
136
+ PRIMARY_SHADES.each do |shade|
137
+ values << "#{base}#{shade}"
138
+ end
139
+ end
140
+
141
+ BASE_ACCENT.each do |base|
142
+ values << "#{base}accent"
143
+ ACCENT_SHADES.each do |shade|
144
+ values << "#{base}accent#{shade}"
145
+ end
146
+ end
147
+
148
+ uniq_map = {}
149
+ values.each { |v| uniq_map[v] = true }
150
+ @all_values = uniq_map.keys.freeze
135
151
  end
136
152
 
137
- def normalize_color(color)
153
+ def self.normalize_color(color)
138
154
  return color.to_s if color.is_a?(Symbol)
139
155
  return color if color.is_a?(String)
140
156
  return color.to_s unless color.respond_to?(:to_s)
@@ -164,15 +180,17 @@ module Ruflet
164
180
  "yellow" => "YELLOW"
165
181
  }.freeze
166
182
 
167
- def constant_prefix_for(base_name)
168
- BASE_PREFIX.fetch(base_name) { base_name.upcase }
183
+ def self.constant_prefix_for(base_name)
184
+ key = base_name.to_s
185
+ return BASE_PREFIX[key] if BASE_PREFIX.key?(key)
186
+ key.upcase
169
187
  end
170
188
 
171
189
  SEMANTIC_COLORS.each { |k, v| const_set(k, v) }
172
190
  FIXED_COLORS.each { |k, v| const_set(k, v) }
173
191
 
174
192
  BASE_PRIMARY.each do |base|
175
- prefix = constant_prefix_for(base)
193
+ prefix = self.constant_prefix_for(base)
176
194
  const_set(prefix, base)
177
195
  PRIMARY_SHADES.each do |shade|
178
196
  const_set("#{prefix}_#{shade}", "#{base}#{shade}")
@@ -180,7 +198,7 @@ module Ruflet
180
198
  end
181
199
 
182
200
  BASE_ACCENT.each do |base|
183
- prefix = "#{constant_prefix_for(base)}_ACCENT"
201
+ prefix = "#{self.constant_prefix_for(base)}_ACCENT"
184
202
  const_set(prefix, "#{base}accent")
185
203
  ACCENT_SHADES.each do |shade|
186
204
  const_set("#{prefix}_#{shade}", "#{base}accent#{shade}")
@@ -191,10 +209,26 @@ module Ruflet
191
209
  const_set(alias_name, const_get(target))
192
210
  end
193
211
 
194
- constants(false).each do |name|
195
- next if respond_to?(name)
196
-
197
- define_singleton_method(name) { const_get(name) }
212
+ constant_names = []
213
+ SEMANTIC_COLORS.each_key { |k| constant_names << k }
214
+ FIXED_COLORS.each_key { |k| constant_names << k }
215
+ BASE_PRIMARY.each do |base|
216
+ constant_names << constant_prefix_for(base).to_sym
217
+ PRIMARY_SHADES.each { |shade| constant_names << "#{constant_prefix_for(base)}_#{shade}".to_sym }
218
+ end
219
+ BASE_ACCENT.each do |base|
220
+ constant_names << "#{constant_prefix_for(base)}_ACCENT".to_sym
221
+ ACCENT_SHADES.each { |shade| constant_names << "#{constant_prefix_for(base)}_ACCENT_#{shade}".to_sym }
222
+ end
223
+ DEPRECATED_ALIASES.each_key { |k| constant_names << k }
224
+
225
+ uniq_constants = {}
226
+ constant_names.each { |n| uniq_constants[n] = true }
227
+ if respond_to?(:define_singleton_method)
228
+ uniq_constants.keys.each do |name|
229
+ next if respond_to?(name)
230
+ define_singleton_method(name) { const_get(name) }
231
+ end
198
232
  end
199
233
  end
200
234
  end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "securerandom"
3
+ begin
4
+ require "securerandom"
5
+ rescue LoadError
6
+ nil
7
+ end
4
8
  require_relative "ui/control_registry"
5
9
  require_relative "icon_data"
6
10
  require_relative "icons/material_icon_lookup"
@@ -17,7 +21,7 @@ module Ruflet
17
21
 
18
22
  def initialize(type:, id: nil, **props)
19
23
  @type = type.to_s.downcase
20
- @id = (id || props.delete(:id) || "ctrl_#{SecureRandom.hex(4)}").to_s
24
+ @id = (id || props.delete(:id) || "ctrl_#{self.class.generate_id}").to_s
21
25
  @children = []
22
26
  @handlers = {}
23
27
  @wire_id = nil
@@ -62,6 +66,16 @@ module Ruflet
62
66
 
63
67
  private
64
68
 
69
+ class << self
70
+ def generate_id
71
+ if defined?(SecureRandom) && SecureRandom.respond_to?(:hex)
72
+ SecureRandom.hex(4)
73
+ else
74
+ format("%08x", rand(0..0xffff_ffff))
75
+ end
76
+ end
77
+ end
78
+
65
79
  def serialize_value(value)
66
80
  case value
67
81
  when Control
@@ -5,6 +5,15 @@ require_relative "ui/control_factory"
5
5
 
6
6
  module Ruflet
7
7
  module DSL
8
+ DURATION_FACTORS_MS = {
9
+ days: 86_400_000.0,
10
+ hours: 3_600_000.0,
11
+ minutes: 60_000.0,
12
+ seconds: 1_000.0,
13
+ milliseconds: 1.0,
14
+ microseconds: 0.001
15
+ }.freeze
16
+
8
17
  module_function
9
18
 
10
19
  def _pending_app
@@ -95,6 +104,7 @@ module Ruflet
95
104
  def cupertinodialogaction(**props) = _pending_app.cupertinodialogaction(**props)
96
105
  def cupertino_navigation_bar(**props) = _pending_app.cupertino_navigation_bar(**props)
97
106
  def cupertinonavigationbar(**props) = _pending_app.cupertinonavigationbar(**props)
107
+ def duration(**parts) = duration_in_milliseconds(parts)
98
108
 
99
109
  class App
100
110
  include UI::ControlMethods
@@ -146,6 +156,10 @@ module Ruflet
146
156
  c
147
157
  end
148
158
 
159
+ def duration(**parts)
160
+ DSL.duration(**parts)
161
+ end
162
+
149
163
  def run
150
164
  app_roots = @roots
151
165
  page_props = @page_props.dup
@@ -177,6 +191,27 @@ module Ruflet
177
191
  "#{type}_#{@seq}"
178
192
  end
179
193
 
194
+ def duration_in_milliseconds(parts)
195
+ DSL.send(:duration_in_milliseconds, parts)
196
+ end
197
+
198
+ end
199
+
200
+ def duration_in_milliseconds(parts)
201
+ return 0 if parts.nil? || parts.empty?
202
+
203
+ DURATION_FACTORS_MS.reduce(0.0) do |sum, (key, factor)|
204
+ sum + read_duration_part(parts, key) * factor
205
+ end.round
206
+ end
207
+
208
+ def read_duration_part(parts, key)
209
+ raw = parts[key] || parts[key.to_s]
210
+ return 0.0 if raw.nil?
211
+ return raw.to_f if raw.is_a?(Numeric)
212
+ return raw.to_f if raw.is_a?(String) && raw.match?(/\A-?\d+(\.\d+)?\z/)
213
+
214
+ 0.0
180
215
  end
181
216
  end
182
217
  end
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "icons/material_icon_lookup"
4
- require_relative "icons/cupertino_icon_lookup"
3
+ begin
4
+ require_relative "icons/material_icon_lookup"
5
+ require_relative "icons/cupertino_icon_lookup"
6
+ rescue StandardError
7
+ nil
8
+ end
5
9
 
6
10
  module Ruflet
7
11
  class IconData
@@ -44,7 +48,8 @@ module Ruflet
44
48
  return codepoint
45
49
  end
46
50
 
47
- raw = input.to_s.strip
51
+ raw = input.to_s
52
+ raw = raw.strip if raw.respond_to?(:strip)
48
53
  return raw if raw.empty?
49
54
 
50
55
  codepoint = Ruflet::MaterialIconLookup.codepoint_for(raw)
@@ -3,7 +3,6 @@
3
3
  require_relative "event"
4
4
  require "ruflet_protocol"
5
5
  require_relative "control"
6
- require_relative "ui/control_methods"
7
6
  require_relative "ui/widget_builder"
8
7
  require_relative "icons/material_icon_lookup"
9
8
  require_relative "icons/cupertino_icon_lookup"
@@ -12,11 +11,21 @@ require "cgi"
12
11
 
13
12
  module Ruflet
14
13
  class Page
15
- include UI::ControlMethods
16
-
17
- PAGE_PROP_KEYS = %w[route title vertical_alignment horizontal_alignment].freeze
14
+ PAGE_PROP_KEYS = %w[route title vertical_alignment horizontal_alignment scroll].freeze
18
15
  DIALOG_PROP_KEYS = %w[dialog snack_bar bottom_sheet].freeze
19
16
  BUTTON_TEXT_TYPES = %w[button elevatedbutton textbutton filledbutton].freeze
17
+ DEPRECATED_PAGE_WIDGET_METHODS = %i[
18
+ control widget view column center row stack container gesture_detector gesturedetector draggable
19
+ drag_target dragtarget text button elevated_button elevatedbutton text_button textbutton filled_button
20
+ filledbutton icon_button iconbutton text_field textfield checkbox radio radio_group radiogroup
21
+ alert_dialog alertdialog markdown icon image app_bar appbar floating_action_button snack_bar snackbar
22
+ bottom_sheet bottomsheet tabs tab tab_bar tabbar tab_bar_view tabbarview navigation_bar navigationbar
23
+ navigation_bar_destination navigationbardestination fab cupertino_button
24
+ cupertinobutton cupertino_filled_button cupertinofilledbutton cupertino_text_field cupertinotextfield
25
+ cupertino_switch cupertinoswitch cupertino_slider cupertinoslider cupertino_alert_dialog
26
+ cupertinoalertdialog cupertino_action_sheet cupertinoactionsheet cupertino_dialog_action
27
+ cupertinodialogaction cupertino_navigation_bar cupertinonavigationbar
28
+ ].freeze
20
29
 
21
30
  attr_reader :session_id, :client_details, :views
22
31
 
@@ -39,12 +48,18 @@ module Ruflet
39
48
  id: "_overlay",
40
49
  controls: []
41
50
  )
51
+ @services_container = Ruflet::Control.new(
52
+ type: "service_registry",
53
+ id: "_services",
54
+ "_services": []
55
+ )
42
56
  @dialogs_container = Ruflet::Control.new(
43
57
  type: "dialogs",
44
58
  id: "_dialogs",
45
59
  controls: []
46
60
  )
47
61
  refresh_overlay_container!
62
+ refresh_services_container!
48
63
  refresh_dialogs_container!
49
64
  end
50
65
 
@@ -123,6 +138,24 @@ module Ruflet
123
138
  self
124
139
  end
125
140
 
141
+ def services
142
+ @services_container.props["_services"] ||= []
143
+ end
144
+
145
+ def services=(value)
146
+ @services_container.props["_services"] = Array(value).compact
147
+ refresh_services_container!
148
+ push_services_update!
149
+ self
150
+ end
151
+
152
+ def add_service(*value)
153
+ @services_container.props["_services"] = services + value.flatten.compact
154
+ refresh_services_container!
155
+ push_services_update!
156
+ self
157
+ end
158
+
126
159
  def go(route, **query_params)
127
160
  @page_props["route"] = build_route(route, query_params)
128
161
  dispatch_page_event(name: "route_change", data: @page_props["route"])
@@ -149,22 +182,10 @@ module Ruflet
149
182
  add(*builder.children)
150
183
  end
151
184
 
152
- def appbar(**props, &block)
153
- return @view_props["appbar"] if props.empty? && !block
154
-
155
- WidgetBuilder.new.appbar(**props, &block)
156
- end
157
-
158
185
  def appbar=(value)
159
186
  @view_props["appbar"] = value
160
187
  end
161
188
 
162
- def floating_action_button(**props, &block)
163
- return @view_props["floating_action_button"] if props.empty? && !block
164
-
165
- WidgetBuilder.new.floating_action_button(**props, &block)
166
- end
167
-
168
189
  def floating_action_button=(value)
169
190
  @view_props["floating_action_button"] = value
170
191
  end
@@ -176,40 +197,20 @@ module Ruflet
176
197
  refresh_dialogs_container!
177
198
  end
178
199
 
179
- def snack_bar(**props, &block)
180
- return @snack_bar if props.empty? && !block
181
-
182
- super
183
- end
184
-
185
200
  def snack_bar=(value)
186
201
  @snack_bar = value
187
202
  refresh_dialogs_container!
188
203
  end
189
204
 
190
- def snackbar(**props, &block)
191
- snack_bar(**props, &block)
192
- end
193
-
194
205
  def snackbar=(value)
195
206
  self.snack_bar = value
196
207
  end
197
208
 
198
- def bottom_sheet(**props, &block)
199
- return @bottom_sheet if props.empty? && !block
200
-
201
- super
202
- end
203
-
204
209
  def bottom_sheet=(value)
205
210
  @bottom_sheet = value
206
211
  refresh_dialogs_container!
207
212
  end
208
213
 
209
- def bottomsheet(**props, &block)
210
- bottom_sheet(**props, &block)
211
- end
212
-
213
214
  def bottomsheet=(value)
214
215
  self.bottom_sheet = value
215
216
  end
@@ -227,6 +228,43 @@ module Ruflet
227
228
  self
228
229
  end
229
230
 
231
+ def invoke(control_or_id, method_name, args: nil, timeout: 10)
232
+ control = resolve_control(control_or_id)
233
+ return nil unless control
234
+
235
+ call_id = "call_#{Ruflet::Control.generate_id}"
236
+ send_message(Protocol::ACTIONS[:invoke_control_method], {
237
+ "control_id" => control.wire_id,
238
+ "call_id" => call_id,
239
+ "name" => method_name.to_s,
240
+ "args" => args,
241
+ "timeout" => timeout
242
+ })
243
+
244
+ call_id
245
+ end
246
+
247
+ def launch_url(url, mode: "external_application", web_view_configuration: nil, browser_configuration: nil, web_only_window_name: nil, timeout: 10)
248
+ launcher = ensure_url_launcher_service
249
+ invoke(
250
+ launcher,
251
+ "launch_url",
252
+ args: {
253
+ "url" => url,
254
+ "mode" => mode,
255
+ "web_view_configuration" => web_view_configuration,
256
+ "browser_configuration" => browser_configuration,
257
+ "web_only_window_name" => web_only_window_name
258
+ }.compact,
259
+ timeout: timeout
260
+ )
261
+ end
262
+
263
+ def can_launch_url(url, timeout: 10)
264
+ launcher = ensure_url_launcher_service
265
+ invoke(launcher, "can_launch_url", args: { "url" => url }, timeout: timeout)
266
+ end
267
+
230
268
  def pop_dialog
231
269
  dialog_control = latest_open_dialog
232
270
  return nil unless dialog_control
@@ -304,6 +342,45 @@ module Ruflet
304
342
  end
305
343
  end
306
344
 
345
+ def method_missing(name, *args, &block)
346
+ method_name = name.to_s
347
+ prop_name = method_name.delete_suffix("=")
348
+
349
+ if method_name.end_with?("=")
350
+ if DEPRECATED_PAGE_WIDGET_METHODS.include?(prop_name.to_sym)
351
+ Kernel.warn("[DEPRECATION] `page.#{prop_name}(...)` is no longer supported.")
352
+ raise NoMethodError, "Use `#{prop_name}(...)` as a free widget helper, then attach with `page.add(...)`."
353
+ end
354
+ assign_split_prop(prop_name, normalize_value(prop_name, args.first))
355
+ return args.first
356
+ end
357
+
358
+ if args.empty? && !block
359
+ return @page_props[method_name] if @page_props.key?(method_name)
360
+ return @view_props[method_name] if @view_props.key?(method_name)
361
+ return instance_variable_get("@#{method_name}") if DIALOG_PROP_KEYS.include?(method_name)
362
+ end
363
+
364
+ if DEPRECATED_PAGE_WIDGET_METHODS.include?(name.to_sym)
365
+ Kernel.warn("[DEPRECATION] `page.#{name}(...)` is no longer supported.")
366
+ raise NoMethodError, "Use `#{name}(...)` as a free widget helper, then attach with `page.add(...)`."
367
+ end
368
+
369
+ super
370
+ end
371
+
372
+ def respond_to_missing?(name, include_private = false)
373
+ method_name = name.to_s
374
+ prop_name = method_name.delete_suffix("=")
375
+ DEPRECATED_PAGE_WIDGET_METHODS.include?(name.to_sym) ||
376
+ DEPRECATED_PAGE_WIDGET_METHODS.include?(prop_name.to_sym) ||
377
+ method_name.end_with?("=") ||
378
+ @page_props.key?(method_name) ||
379
+ @view_props.key?(method_name) ||
380
+ DIALOG_PROP_KEYS.include?(method_name) ||
381
+ super
382
+ end
383
+
307
384
  private
308
385
 
309
386
  def build_widget(type, **props, &block) = WidgetBuilder.new.control(type, **props, &block)
@@ -480,6 +557,23 @@ module Ruflet
480
557
  @page_props["_overlay"] = @overlay_container
481
558
  end
482
559
 
560
+ def refresh_services_container!
561
+ @page_props["_services"] = @services_container
562
+ end
563
+
564
+ def push_services_update!
565
+ refresh_control_indexes!
566
+
567
+ if @services_container.wire_id
568
+ send_message(Protocol::ACTIONS[:patch_control], {
569
+ "id" => @services_container.wire_id,
570
+ "patch" => [[0], [0, 0, "_services", serialize_patch_value(@services_container.props["_services"])]]
571
+ })
572
+ else
573
+ send_view_patch
574
+ end
575
+ end
576
+
483
577
  def push_dialogs_update!
484
578
  refresh_control_indexes!
485
579
 
@@ -546,5 +640,14 @@ module Ruflet
546
640
  end
547
641
  codepoint
548
642
  end
643
+
644
+ def ensure_url_launcher_service
645
+ launcher = services.find { |service| service.is_a?(Control) && service.type == "url_launcher" }
646
+ return launcher if launcher
647
+
648
+ launcher = build_widget(:url_launcher)
649
+ add_service(launcher)
650
+ launcher
651
+ end
549
652
  end
550
653
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "material_control_factory"
4
4
  require_relative "cupertino_control_factory"
5
+ require_relative "../control"
5
6
 
6
7
  module Ruflet
7
8
  module UI
@@ -13,9 +14,9 @@ module Ruflet
13
14
  def build(type, id: nil, **props)
14
15
  normalized_type = type.to_s.downcase
15
16
  klass = CLASS_MAP[normalized_type]
16
- raise ArgumentError, "Unsupported control type: #{normalized_type}" unless klass
17
+ return klass.new(id: id, **props) if klass
17
18
 
18
- klass.new(id: id, **props)
19
+ Ruflet::Control.new(type: normalized_type, id: id, **props)
19
20
  end
20
21
  end
21
22
  end
@@ -7,6 +7,17 @@ module Ruflet
7
7
  def initialize(id: nil, **props)
8
8
  super(type: "cupertino_dialog_action", id: id, **props)
9
9
  end
10
+
11
+ private
12
+
13
+ def preprocess_props(props)
14
+ mapped = props.dup
15
+ if mapped.key?(:text) || mapped.key?("text")
16
+ value = mapped.key?(:text) ? mapped.delete(:text) : mapped.delete("text")
17
+ mapped[:content] = value unless mapped.key?(:content) || mapped.key?("content")
18
+ end
19
+ mapped
20
+ end
10
21
  end
11
22
  end
12
23
  end
@@ -7,6 +7,61 @@ module Ruflet
7
7
  def initialize(id: nil, **props)
8
8
  super(type: "snackbar", id: id, **props)
9
9
  end
10
+
11
+ private
12
+
13
+ def preprocess_props(props)
14
+ mapped = props.dup
15
+ key = if mapped.key?(:duration)
16
+ :duration
17
+ elsif mapped.key?("duration")
18
+ "duration"
19
+ end
20
+ return mapped unless key
21
+
22
+ mapped[key] = normalize_duration_ms(mapped[key])
23
+ mapped
24
+ end
25
+
26
+ def normalize_duration_ms(value)
27
+ return value.to_i if value.is_a?(Numeric)
28
+ return value.to_i if value.is_a?(String) && value.match?(/\A\d+(\.\d+)?\z/)
29
+
30
+ parts =
31
+ if value.is_a?(Hash)
32
+ value
33
+ elsif value.respond_to?(:to_h)
34
+ value.to_h
35
+ else
36
+ nil
37
+ end
38
+ return value unless parts.is_a?(Hash)
39
+
40
+ days = read_number(parts, "days")
41
+ hours = read_number(parts, "hours")
42
+ minutes = read_number(parts, "minutes")
43
+ seconds = read_number(parts, "seconds")
44
+ milliseconds = read_number(parts, "milliseconds")
45
+ microseconds = read_number(parts, "microseconds")
46
+
47
+ total_ms = 0.0
48
+ total_ms += days * 86_400_000.0
49
+ total_ms += hours * 3_600_000.0
50
+ total_ms += minutes * 60_000.0
51
+ total_ms += seconds * 1_000.0
52
+ total_ms += milliseconds
53
+ total_ms += microseconds / 1000.0
54
+ total_ms.round
55
+ end
56
+
57
+ def read_number(parts, key)
58
+ raw = parts[key] || parts[key.to_sym]
59
+ return 0.0 if raw.nil?
60
+ return raw.to_f if raw.is_a?(Numeric)
61
+ return raw.to_f if raw.is_a?(String) && raw.match?(/\A-?\d+(\.\d+)?\z/)
62
+
63
+ 0.0
64
+ end
10
65
  end
11
66
  end
12
67
  end
@@ -8,18 +8,32 @@ module Ruflet
8
8
  "cupertinobutton" => "CupertinoButton",
9
9
  "cupertino_filled_button" => "CupertinoFilledButton",
10
10
  "cupertinofilledbutton" => "CupertinoFilledButton",
11
+ "cupertino_tinted_button" => "CupertinoTintedButton",
12
+ "cupertinotintedbutton" => "CupertinoTintedButton",
11
13
  "cupertino_text_field" => "CupertinoTextField",
12
14
  "cupertinotextfield" => "CupertinoTextField",
15
+ "cupertino_checkbox" => "CupertinoCheckbox",
16
+ "cupertinocheckbox" => "CupertinoCheckbox",
13
17
  "cupertino_switch" => "CupertinoSwitch",
14
18
  "cupertinoswitch" => "CupertinoSwitch",
15
19
  "cupertino_slider" => "CupertinoSlider",
16
20
  "cupertinoslider" => "CupertinoSlider",
21
+ "cupertino_radio" => "CupertinoRadio",
22
+ "cupertinoradio" => "CupertinoRadio",
17
23
  "cupertino_alert_dialog" => "CupertinoAlertDialog",
18
24
  "cupertinoalertdialog" => "CupertinoAlertDialog",
19
25
  "cupertino_action_sheet" => "CupertinoActionSheet",
20
26
  "cupertinoactionsheet" => "CupertinoActionSheet",
21
27
  "cupertino_dialog_action" => "CupertinoDialogAction",
22
28
  "cupertinodialogaction" => "CupertinoDialogAction",
29
+ "cupertino_bottom_sheet" => "CupertinoBottomSheet",
30
+ "cupertinobottomsheet" => "CupertinoBottomSheet",
31
+ "cupertino_picker" => "CupertinoPicker",
32
+ "cupertinopicker" => "CupertinoPicker",
33
+ "cupertino_date_picker" => "CupertinoDatePicker",
34
+ "cupertinodatepicker" => "CupertinoDatePicker",
35
+ "cupertino_timer_picker" => "CupertinoTimerPicker",
36
+ "cupertinotimerpicker" => "CupertinoTimerPicker",
23
37
  "cupertino_navigation_bar" => "CupertinoNavigationBar",
24
38
  "cupertinonavigationbar" => "CupertinoNavigationBar"
25
39
  }.freeze
@@ -71,14 +71,17 @@ module Ruflet
71
71
 
72
72
  def icon(**props) = build_widget(:icon, **props)
73
73
 
74
- def image(src = nil, **props)
74
+ def image(src = nil, src_base64: nil, placeholder_src: nil, **props)
75
75
  mapped = props.dup
76
- mapped[:src] = src unless src.nil?
76
+ mapped[:src] = normalize_image_source(src) unless src.nil?
77
+ mapped[:src] = normalize_image_source(src_base64) if mapped[:src].nil? && !src_base64.nil?
78
+ mapped[:placeholder_src] = normalize_image_source(placeholder_src) unless placeholder_src.nil?
77
79
  build_widget(:image, **mapped)
78
80
  end
79
81
 
80
82
  def app_bar(**props) = build_widget(:appbar, **props)
81
83
  def appbar(**props) = app_bar(**props)
84
+ def url_launcher(**props) = build_widget(:url_launcher, **props)
82
85
  def floating_action_button(**props) = build_widget(:floatingactionbutton, **props)
83
86
  def floatingactionbutton(**props) = floating_action_button(**props)
84
87
  def tabs(**props, &block) = build_widget(:tabs, **props, &block)
@@ -100,6 +103,12 @@ module Ruflet
100
103
 
101
104
  private
102
105
 
106
+ def normalize_image_source(value)
107
+ return value unless value.is_a?(Array)
108
+ return value.pack("C*") if value.all? { |v| v.is_a?(Integer) }
109
+ value
110
+ end
111
+
103
112
  # Flet container alignment expects a vector-like object ({x:, y:}),
104
113
  # not a plain string. Keep common shorthand compatible.
105
114
  def normalize_container_props(props)
@@ -52,7 +52,55 @@ module Ruflet
52
52
  "navigationbar" => "NavigationBar",
53
53
  "navigation_bar" => "NavigationBar",
54
54
  "navigationbardestination" => "NavigationBarDestination",
55
- "navigation_bar_destination" => "NavigationBarDestination"
55
+ "navigation_bar_destination" => "NavigationBarDestination",
56
+ "switch" => "Switch",
57
+ "slider" => "Slider",
58
+ "dropdown" => "DropdownM2",
59
+ "dropdownm2" => "DropdownM2",
60
+ "dropdown_m2" => "DropdownM2",
61
+ "option" => "Option",
62
+ "card" => "Card",
63
+ "banner" => "Banner",
64
+ "datepicker" => "DatePicker",
65
+ "date_picker" => "DatePicker",
66
+ "timepicker" => "TimePicker",
67
+ "time_picker" => "TimePicker",
68
+ "filledtonalbutton" => "FilledTonalButton",
69
+ "filled_tonal_button" => "FilledTonalButton",
70
+ "outlinedbutton" => "OutlinedButton",
71
+ "outlined_button" => "OutlinedButton",
72
+ "listtile" => "ListTile",
73
+ "list_tile" => "ListTile",
74
+ "progressbar" => "ProgressBar",
75
+ "progress_bar" => "ProgressBar",
76
+ "safearea" => "SafeArea",
77
+ "safe_area" => "SafeArea",
78
+ "canvas" => "Canvas",
79
+ "line" => "Line",
80
+ "service_registry" => "ServiceRegistry",
81
+ "url_launcher" => "UrlLauncher",
82
+ "audio" => "Audio",
83
+ "video" => "Video",
84
+ "flashlight" => "Flashlight",
85
+ "barchart" => "BarChart",
86
+ "barchartgroup" => "BarChartGroup",
87
+ "barchartrod" => "BarChartRod",
88
+ "barchartrodstackitem" => "BarChartRodStackItem",
89
+ "linechart" => "LineChart",
90
+ "linechartdata" => "LineChartData",
91
+ "linechartdatapoint" => "LineChartDataPoint",
92
+ "piechart" => "PieChart",
93
+ "piechartsection" => "PieChartSection",
94
+ "candlestickchart" => "CandlestickChart",
95
+ "candlestickchartspot" => "CandlestickChartSpot",
96
+ "radarchart" => "RadarChart",
97
+ "radarcharttitle" => "RadarChartTitle",
98
+ "radardataset" => "RadarDataSet",
99
+ "radardatasetentry" => "RadarDataSetEntry",
100
+ "scatterchart" => "ScatterChart",
101
+ "scatterchartspot" => "ScatterChartSpot",
102
+ "chartaxis" => "ChartAxis",
103
+ "chartaxislabel" => "ChartAxisLabel"
56
104
  }.freeze
57
105
 
58
106
  EVENT_PROPS = {
@@ -77,6 +125,21 @@ module Ruflet
77
125
  on_horizontal_drag_start: "horizontal_drag_start",
78
126
  on_horizontal_drag_update: "horizontal_drag_update",
79
127
  on_horizontal_drag_end: "horizontal_drag_end",
128
+ on_tap_down: "tap_down",
129
+ on_long_press_start: "long_press_start",
130
+ on_right_pan_start: "right_pan_start",
131
+ on_event: "event",
132
+ on_load: "load",
133
+ on_loaded: "loaded",
134
+ on_enter_fullscreen: "enter_fullscreen",
135
+ on_exit_fullscreen: "exit_fullscreen",
136
+ on_duration_change: "duration_change",
137
+ on_position_change: "position_change",
138
+ on_state_change: "state_change",
139
+ on_seek_complete: "seek_complete",
140
+ on_complete: "complete",
141
+ on_track_change: "track_change",
142
+ on_error: "error",
80
143
  on_accept: "accept",
81
144
  on_will_accept: "will_accept",
82
145
  on_accept_with_details: "accept_with_details",
@@ -74,6 +74,7 @@ module Ruflet
74
74
  def cupertinodialogaction(**props) = control_delegate.cupertinodialogaction(**props)
75
75
  def cupertino_navigation_bar(**props) = control_delegate.cupertino_navigation_bar(**props)
76
76
  def cupertinonavigationbar(**props) = control_delegate.cupertinonavigationbar(**props)
77
+ def duration(**parts) = control_delegate.duration(**parts)
77
78
 
78
79
  private
79
80
 
data/lib/ruflet_ui.rb CHANGED
@@ -105,5 +105,7 @@ module Kernel
105
105
  Ruflet::DSL
106
106
  end
107
107
 
108
- private(*Ruflet::UI::SharedControlForwarders.instance_methods(false))
108
+ if Ruflet::UI::SharedControlForwarders.respond_to?(:instance_methods)
109
+ private(*Ruflet::UI::SharedControlForwarders.instance_methods(false))
110
+ end
109
111
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa
@@ -19,6 +19,7 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - README.md
21
21
  - lib/ruflet.rb
22
+ - lib/ruflet/manifest_compiler.rb
22
23
  - lib/ruflet/version.rb
23
24
  - lib/ruflet_protocol.rb
24
25
  - lib/ruflet_protocol/ruflet/protocol.rb