From b248f9cd8a1160a7b0577870ffcc6ffa96331f3b Mon Sep 17 00:00:00 2001 From: AdamMusa Date: Mon, 9 Mar 2026 03:43:20 -0400 Subject: [PATCH 1/4] added multiple port support so we can run each platform in different port --- packages/ruflet/lib/ruflet_ui.rb | 2 +- packages/ruflet/lib/ruflet_ui/ruflet/app.rb | 13 ++++++-- packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb | 19 ++++++++--- .../ruflet/ui/material_control_methods.rb | 5 +-- .../ruflet/ui/shared_control_forwarders.rb | 1 - packages/ruflet_cli/lib/ruflet/cli.rb | 2 +- .../ruflet_cli/lib/ruflet/cli/run_command.rb | 31 ++++++++++-------- packages/ruflet_cli/ruflet_cli-0.0.7.gem | Bin 15872 -> 15872 bytes ruflet_client/lib/main.dart | 15 ++++----- 9 files changed, 54 insertions(+), 34 deletions(-) diff --git a/packages/ruflet/lib/ruflet_ui.rb b/packages/ruflet/lib/ruflet_ui.rb index 9f800b1e..d1a44f51 100644 --- a/packages/ruflet/lib/ruflet_ui.rb +++ b/packages/ruflet/lib/ruflet_ui.rb @@ -79,7 +79,7 @@ def [](name) class << self include UI::SharedControlForwarders - def app(host: "0.0.0.0", port: 8550, &block) + def app(host: nil, port: nil, &block) DSL.app(host: host, port: port, &block) end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/app.rb b/packages/ruflet/lib/ruflet_ui/ruflet/app.rb index c7917901..2469312e 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/app.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/app.rb @@ -2,9 +2,9 @@ module Ruflet class App - def initialize(host: "0.0.0.0", port: 8550) - @host = host - @port = port + def initialize(host: nil, port: nil) + @host = (host || ENV["RUFLET_HOST"] || "0.0.0.0") + @port = normalize_port(port || ENV["RUFLET_PORT"] || 8550) end def run @@ -16,5 +16,12 @@ def run def view(_page) raise NotImplementedError, "#{self.class} must implement #view(page)" end + + private + + def normalize_port(value) + port = value.to_i + port > 0 ? port : 8550 + end end end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb b/packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb index d04396d9..416b7bde 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb @@ -16,15 +16,27 @@ module DSL module_function + def default_host + ENV["RUFLET_HOST"].to_s.strip.empty? ? "0.0.0.0" : ENV["RUFLET_HOST"].to_s + end + + def default_port + raw = ENV["RUFLET_PORT"].to_s + value = raw.to_i + value > 0 ? value : 8550 + end + def _pending_app - @_pending_app ||= App.new(host: "0.0.0.0", port: 8550) + @_pending_app ||= App.new(host: default_host, port: default_port) end def _reset_pending_app! - @_pending_app = App.new(host: "0.0.0.0", port: 8550) + @_pending_app = App.new(host: default_host, port: default_port) end - def app(host: "0.0.0.0", port: 8550, &block) + def app(host: nil, port: nil, &block) + host ||= default_host + port ||= default_port return App.new(host: host, port: port).tap { |a| a.instance_eval(&block) } if block pending = _pending_app @@ -52,7 +64,6 @@ def dragtarget(**props, &block) = _pending_app.dragtarget(**props, &block) def text(value = nil, **props) = _pending_app.text(value, **props) def button(**props) = _pending_app.button(**props) def elevated_button(**props) = _pending_app.elevated_button(**props) - def elevatedbutton(**props) = _pending_app.elevatedbutton(**props) def text_field(**props) = _pending_app.text_field(**props) def textfield(**props) = _pending_app.textfield(**props) def icon(**props) = _pending_app.icon(**props) diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/material_control_methods.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/material_control_methods.rb index 89355bd1..aab4b14b 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/material_control_methods.rb @@ -43,8 +43,9 @@ def text(value = nil, **props) end def button(**props) = build_widget(:button, **props) - def elevated_button(**props) = build_widget(:elevatedbutton, **props) - def elevatedbutton(**props) = elevated_button(**props) + # Ruflet currently uses a single Material button control schema. + # Keep elevated_button DSL available by routing to :button. + def elevated_button(**props) = build_widget(:button, **props) def text_button(**props) = build_widget(:textbutton, **props) def textbutton(**props) = text_button(**props) def filled_button(**props) = build_widget(:filledbutton, **props) diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb index c57676b1..669f7d74 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb @@ -22,7 +22,6 @@ def dragtarget(**props, &block) = control_delegate.dragtarget(**props, &block) def text(value = nil, **props) = control_delegate.text(value, **props) def button(**props) = control_delegate.button(**props) def elevated_button(**props) = control_delegate.elevated_button(**props) - def elevatedbutton(**props) = control_delegate.elevatedbutton(**props) def text_button(**props) = control_delegate.text_button(**props) def textbutton(**props) = control_delegate.textbutton(**props) def filled_button(**props) = control_delegate.filled_button(**props) diff --git a/packages/ruflet_cli/lib/ruflet/cli.rb b/packages/ruflet_cli/lib/ruflet/cli.rb index b5e2beb3..2dd3c13d 100644 --- a/packages/ruflet_cli/lib/ruflet/cli.rb +++ b/packages/ruflet_cli/lib/ruflet/cli.rb @@ -54,7 +54,7 @@ def print_help Commands: ruflet create ruflet new - ruflet run [scriptname|path] [--web|--desktop] + ruflet run [scriptname|path] [--web|--desktop] [--port PORT] ruflet debug [scriptname|path] ruflet build ruflet devices diff --git a/packages/ruflet_cli/lib/ruflet/cli/run_command.rb b/packages/ruflet_cli/lib/ruflet/cli/run_command.rb index 0bc0b53b..a5edcbc2 100644 --- a/packages/ruflet_cli/lib/ruflet/cli/run_command.rb +++ b/packages/ruflet_cli/lib/ruflet/cli/run_command.rb @@ -16,10 +16,11 @@ module Ruflet module CLI module RunCommand def command_run(args) - options = { target: "mobile" } + options = { target: "mobile", requested_port: 8550 } parser = OptionParser.new do |o| o.on("--web") { options[:target] = "web" } o.on("--desktop") { options[:target] = "desktop" } + o.on("--port PORT", Integer) { |v| options[:requested_port] = v } end parser.parse!(args) @@ -31,7 +32,7 @@ def command_run(args) return 1 end - selected_port = resolve_backend_port(options[:target]) + selected_port = resolve_backend_port(options[:target], requested_port: options[:requested_port]) return 1 unless selected_port env = { "RUFLET_TARGET" => options[:target], @@ -41,7 +42,7 @@ def command_run(args) assets_dir = File.join(File.dirname(script_path), "assets") env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir) - print_run_banner(target: options[:target], port: selected_port) + print_run_banner(target: options[:target], requested_port: options[:requested_port], port: selected_port) print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile" gemfile_path = find_nearest_gemfile(Dir.pwd) @@ -126,12 +127,14 @@ def find_nearest_gemfile(start_dir) end end - def print_run_banner(target:, port:) - if target == "mobile" && port != 8550 - puts "Requested port 8550 is busy; bound to #{port}" + def print_run_banner(target:, requested_port:, port:) + if port != requested_port.to_i + puts "Requested port #{requested_port} is busy; bound to #{port}" end if target == "desktop" puts "Ruflet desktop URL: http://localhost:#{port}" + elsif target == "mobile" + puts "Ruflet target: #{target}" else puts "Ruflet target: #{target}" puts "Ruflet URL: http://localhost:#{port}" @@ -162,9 +165,11 @@ def launch_web_client(port) web_pid = Process.spawn("python3", "-m", "http.server", web_port.to_s, "--bind", "127.0.0.1", chdir: web_dir, out: File::NULL, err: File::NULL) Process.detach(web_pid) wait_for_server_boot(web_port) - browser_pid = open_in_browser_app_mode("http://localhost:#{web_port}") - open_in_browser("http://localhost:#{web_port}") if browser_pid.nil? - puts "Ruflet web client: http://localhost:#{web_port}" + backend_url = "http://localhost:#{port}" + web_url = "http://localhost:#{web_port}/?#{URI.encode_www_form(url: backend_url)}" + browser_pid = open_in_browser_app_mode(web_url) + open_in_browser(web_url) if browser_pid.nil? + puts "Ruflet web client: #{web_url}" puts "Ruflet backend ws: ws://localhost:#{port}/ws" [web_pid, browser_pid].compact rescue Errno::ENOENT @@ -532,8 +537,6 @@ def print_mobile_qr_hint(port: 8550) puts puts "Ruflet mobile connect URL:" puts " #{payload}" - puts "Ruflet server ws URL:" - puts " ws://0.0.0.0:#{port}/ws" puts "Scan this QR from ruflet_client (Connect -> Scan QR):" print_ascii_qr(payload) puts @@ -561,8 +564,10 @@ def find_available_port(start_port, max_attempts: 100) start_port end - def resolve_backend_port(target) - find_available_port(8550) + def resolve_backend_port(_target, requested_port: 8550) + base = requested_port.to_i + base = 8550 if base <= 0 + find_available_port(base) end def port_available?(port) diff --git a/packages/ruflet_cli/ruflet_cli-0.0.7.gem b/packages/ruflet_cli/ruflet_cli-0.0.7.gem index 2e2bd2ed34634957166c45205d57f616d7893509..08af774f056e3446017b3974c59c401833c0cd41 100644 GIT binary patch delta 10252 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 SKMdN{J=6iSF*(l!ksuArncsE* delta 10154 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 YJX>c3e*6O|rbeRF0kbhV&jgVm3&x1T9{>OV diff --git a/ruflet_client/lib/main.dart b/ruflet_client/lib/main.dart index d4083629..9a12f88c 100644 --- a/ruflet_client/lib/main.dart +++ b/ruflet_client/lib/main.dart @@ -169,6 +169,12 @@ void main([List? args]) async { if (routeUrlStrategy == 'path') { usePathUrlStrategy(); } + final queryUrl = Uri.base.queryParameters['url']; + if (queryUrl != null && queryUrl.trim().isNotEmpty) { + initialUrl = queryUrl; + } else if (!kDebugMode) { + initialUrl = 'http://localhost:8550'; + } } else { if (args != null && args.isNotEmpty) { initialUrl = args[0]; @@ -182,15 +188,6 @@ void main([List? args]) async { } } - final isDesktop = - !kIsWeb && - (defaultTargetPlatform == TargetPlatform.windows || - defaultTargetPlatform == TargetPlatform.macOS || - defaultTargetPlatform == TargetPlatform.linux); - if (kIsWeb || isDesktop) { - initialUrl = 'http://localhost:8550'; - } - initialUrl = normalizePageUrlForPlatform(initialUrl); debugPrint('Initial URL: $initialUrl'); From 9cae290a409bef452007a95248a2f0b7e58f5dff Mon Sep 17 00:00:00 2001 From: AdamMusa Date: Mon, 9 Mar 2026 04:12:44 -0400 Subject: [PATCH 2/4] added ruflet.yaml support for release server driven native ui --- .../lib/ruflet/cli/build_command.rb | 209 ++++++++++++++---- .../ruflet_cli/lib/ruflet/cli/new_command.rb | 145 ++++++++++++ packages/ruflet_cli/test/new_command_test.rb | 50 +++++ .../ruflet_flutter_template/lib/main.dart | 10 +- 4 files changed, 374 insertions(+), 40 deletions(-) diff --git a/packages/ruflet_cli/lib/ruflet/cli/build_command.rb b/packages/ruflet_cli/lib/ruflet/cli/build_command.rb index 028a982b..6babed3e 100644 --- a/packages/ruflet_cli/lib/ruflet/cli/build_command.rb +++ b/packages/ruflet_cli/lib/ruflet/cli/build_command.rb @@ -1,12 +1,31 @@ # frozen_string_literal: true require "fileutils" +require "uri" require "yaml" module Ruflet module CLI module BuildCommand include FlutterSdk + CLIENT_EXTENSION_MAP = { + "ads" => { package: "flet_ads", alias: "flet_ads" }, + "audio" => { package: "flet_audio", alias: "flet_audio" }, + "audio_recorder" => { package: "flet_audio_recorder", alias: "flet_audio_recorder" }, + "camera" => { package: "flet_camera", alias: "flet_camera" }, + "charts" => { package: "flet_charts", alias: "flet_charts" }, + "code_editor" => { package: "flet_code_editor", alias: "flet_code_editor" }, + "color_pickers" => { package: "flet_color_pickers", alias: "flet_color_picker" }, + "datatable2" => { package: "flet_datatable2", alias: "flet_datatable2" }, + "flashlight" => { package: "flet_flashlight", alias: "flet_flashlight" }, + "geolocator" => { package: "flet_geolocator", alias: "flet_geolocator" }, + "lottie" => { package: "flet_lottie", alias: "flet_lottie" }, + "map" => { package: "flet_map", alias: "flet_map" }, + "permission_handler" => { package: "flet_permission_handler", alias: "flet_permission_handler" }, + "secure_storage" => { package: "flet_secure_storage", alias: "flet_secure_storage" }, + "video" => { package: "flet_video", alias: "flet_video" }, + "webview" => { package: "flet_webview", alias: "flet_webview" } + }.freeze def command_build(args) platform = (args.shift || "").downcase @@ -28,11 +47,18 @@ def command_build(args) return 1 end + config = load_ruflet_config tools = ensure_flutter!("build", client_dir: client_dir) - ok = prepare_flutter_client(client_dir, tools: tools) + ok = prepare_flutter_client(client_dir, tools: tools, config: config) return 1 unless ok - ok = system(tools[:env], tools[:flutter], *flutter_cmd, *args, chdir: client_dir) + build_args = [*flutter_cmd, *args] + backend_url = configured_backend_url(config) + if backend_url + build_args += ["--dart-define", "RUFLET_BACKEND_URL=#{backend_url}"] + end + + ok = system(tools[:env], tools[:flutter], *build_args, chdir: client_dir) ok ? 0 : 1 end @@ -51,35 +77,72 @@ def detect_flutter_client_dir nil end - def prepare_flutter_client(client_dir, tools:) - apply_build_config(client_dir) + def prepare_flutter_client(client_dir, tools:, config:) + apply_service_extension_config(client_dir, config) + asset_flags = apply_build_config(client_dir, config) + if asset_flags[:error] + warn asset_flags[:error] + return false + end unless system(tools[:env], tools[:flutter], "pub", "get", chdir: client_dir) warn "flutter pub get failed" return false end - unless system(tools[:env], tools[:dart], "run", "flutter_native_splash:create", chdir: client_dir) - warn "flutter_native_splash failed" - return false + if asset_flags[:has_splash] + unless system(tools[:env], tools[:dart], "run", "flutter_native_splash:create", chdir: client_dir) + warn "flutter_native_splash failed" + return false + end end - unless system(tools[:env], tools[:dart], "run", "flutter_launcher_icons", chdir: client_dir) - warn "flutter_launcher_icons failed" - return false + if asset_flags[:has_icon] + unless system(tools[:env], tools[:dart], "run", "flutter_launcher_icons", chdir: client_dir) + warn "flutter_launcher_icons failed" + return false + end end true end - def apply_build_config(client_dir) + def configured_backend_url(config) + candidates = [ + config["backend_url"], + (config["app"].is_a?(Hash) ? config["app"]["backend_url"] : nil) + ] + raw = candidates.find { |v| !v.to_s.strip.empty? } + return nil if raw.nil? + + value = raw.to_s.strip + uri = URI.parse(value) + return nil unless %w[http https ws wss].include?(uri.scheme) + return nil if uri.host.to_s.strip.empty? + + value + rescue URI::InvalidURIError + nil + end + + def load_ruflet_config config_path = ENV["RUFLET_CONFIG"] || "ruflet.yaml" unless File.file?(config_path) alt = "ruflet.yml" config_path = alt if File.file?(alt) end + return {} unless File.file?(config_path) + YAML.safe_load(File.read(config_path), aliases: true) || {} + rescue StandardError => e + warn "Failed to load ruflet config: #{e.class}: #{e.message}" + {} + end + + def apply_build_config(client_dir, config = {}) + build = config["build"] || {} + assets = config["assets"] || {} + config_path = ENV["RUFLET_CONFIG"] || (File.file?("ruflet.yaml") ? "ruflet.yaml" : "ruflet.yml") config_present = File.file?(config_path) - config = config_present ? (YAML.load_file(config_path) || {}) : {} build = config["build"] || {} assets = config["assets"] || {} config_dir = config_present ? File.dirname(File.expand_path(config_path)) : Dir.pwd @@ -87,44 +150,24 @@ def apply_build_config(client_dir) assets_root = build["assets_dir"] || assets["dir"] || config["assets_dir"] || "assets" assets_root = File.expand_path(assets_root, config_dir) - unless config_present || Dir.exist?(assets_root) || ENV["RUFLET_SPLASH"] || ENV["RUFLET_ICON"] - return - end - resolve_asset = lambda do |path| return nil if path.nil? || path.to_s.strip.empty? full = File.expand_path(path.to_s, config_dir) File.file?(full) ? full : nil end - find_first = lambda do |dir, names| - names.each do |name| - candidate = File.join(dir, name) - return candidate if File.file?(candidate) - end - nil - end + splash_defined = key_defined?(build, "splash_screen") || key_defined?(assets, "splash_screen") || key_defined?(config, "splash_screen") + icon_defined = key_defined?(build, "icon_launcher") || key_defined?(assets, "icon_launcher") || key_defined?(config, "icon_launcher") - splash = resolve_asset.call(build["splash"] || assets["splash"] || ENV["RUFLET_SPLASH"]) + splash = resolve_asset.call(build["splash_screen"] || assets["splash_screen"] || config["splash_screen"]) splash_dark = resolve_asset.call(build["splash_dark"] || build["splash_dark_image"] || assets["splash_dark"]) - icon = resolve_asset.call(build["icon"] || assets["icon"] || ENV["RUFLET_ICON"]) + icon = resolve_asset.call(build["icon_launcher"] || assets["icon_launcher"] || config["icon_launcher"]) icon_android = resolve_asset.call(build["icon_android"] || assets["icon_android"]) icon_ios = resolve_asset.call(build["icon_ios"] || assets["icon_ios"]) icon_web = resolve_asset.call(build["icon_web"] || assets["icon_web"]) icon_windows = resolve_asset.call(build["icon_windows"] || assets["icon_windows"]) icon_macos = resolve_asset.call(build["icon_macos"] || assets["icon_macos"]) - if Dir.exist?(assets_root) - splash ||= find_first.call(assets_root, ["splash.png", "splash.jpg", "splash.webp", "splash.bmp"]) - splash_dark ||= find_first.call(assets_root, ["splash_dark.png", "splash_dark.jpg", "splash_dark.webp", "splash_dark.bmp"]) - icon ||= find_first.call(assets_root, ["icon.png", "icon.jpg", "icon.webp", "icon.bmp"]) - icon_android ||= find_first.call(assets_root, ["icon_android.png", "icon_android.jpg", "icon_android.webp"]) - icon_ios ||= find_first.call(assets_root, ["icon_ios.png", "icon_ios.jpg", "icon_ios.webp"]) - icon_web ||= find_first.call(assets_root, ["icon_web.png", "icon_web.jpg", "icon_web.webp"]) - icon_windows ||= find_first.call(assets_root, ["icon_windows.ico", "icon_windows.png"]) - icon_macos ||= find_first.call(assets_root, ["icon_macos.png", "icon_macos.jpg", "icon_macos.webp"]) - end - splash_color = build["splash_color"] splash_dark_color = build["splash_dark_color"] || build["splash_color_dark"] icon_background = build["icon_background"] @@ -150,10 +193,19 @@ def apply_build_config(client_dir) end copy_asset.call(icon_macos, "icon_macos.png") + if splash_defined && splash.nil? + return { has_icon: false, has_splash: false, error: "build config error: splash_screen is set but file was not found" } + end + if icon_defined && icon.nil? + return { has_icon: false, has_splash: false, error: "build config error: icon_launcher is set but file was not found" } + end + pubspec_path = File.join(client_dir, "pubspec.yaml") - return unless File.file?(pubspec_path) + unless File.file?(pubspec_path) + return { has_icon: icon_defined && !icon.nil?, has_splash: splash_defined && !splash.nil?, error: nil } + end - if icon + if icon_defined && icon update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path", "\"assets/icon.png\"", multiple: true) end update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_android", "\"assets/icon_android.png\"", multiple: true) if icon_android @@ -168,10 +220,89 @@ def apply_build_config(client_dir) update_pubspec_value(pubspec_path, "flutter_launcher_icons", "background_color", "\"#{icon_background}\"") if icon_background update_pubspec_value(pubspec_path, "flutter_launcher_icons", "theme_color", "\"#{theme_color}\"") if theme_color - update_pubspec_value(pubspec_path, "flutter_native_splash", "image", "\"assets/splash.png\"") if splash + update_pubspec_value(pubspec_path, "flutter_native_splash", "image", "\"assets/splash.png\"") if splash_defined && splash update_pubspec_value(pubspec_path, "flutter_native_splash", "image_dark", "\"assets/splash_dark.png\"") if splash_dark update_pubspec_value(pubspec_path, "flutter_native_splash", "color", "\"#{splash_color}\"") if splash_color update_pubspec_value(pubspec_path, "flutter_native_splash", "color_dark", "\"#{splash_dark_color}\"") if splash_dark_color + + { has_icon: icon_defined && !icon.nil?, has_splash: splash_defined && !splash.nil?, error: nil } + end + + def key_defined?(hash, key) + hash.is_a?(Hash) && (hash.key?(key) || hash.key?(key.to_sym)) + end + + def apply_service_extension_config(client_dir, config = {}) + services = Array(config["services"]) + extension_keys = services.map { |v| normalize_extension_key(v) }.compact.uniq + extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq + extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq + + pubspec_path = File.join(client_dir, "pubspec.yaml") + main_path = File.join(client_dir, "lib", "main.dart") + prune_client_pubspec(pubspec_path, extension_packages) if File.file?(pubspec_path) + prune_client_main(main_path, extension_aliases) if File.file?(main_path) + end + + def normalize_extension_key(value) + key = value.to_s.strip.downcase + return nil if key.empty? + + key.tr!("-", "_") + key.gsub!(/\A(flet_)+/, "") + key.gsub!(/\Aservice_/, "") + key + end + + def prune_client_pubspec(path, selected_packages) + data = YAML.safe_load(File.read(path), aliases: true) || {} + deps = (data["dependencies"] || {}).dup + + deps.keys.each do |name| + next unless name.start_with?("flet_") + next if name == "flet" + next if selected_packages.include?(name) + + deps.delete(name) + end + + data["dependencies"] = deps + File.write(path, YAML.dump(data)) + end + + def prune_client_main(path, selected_aliases) + lines = File.readlines(path) + alias_to_package = {} + + lines.each do |line| + match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);}) + next unless match + + alias_to_package[match[2]] = match[1] + end + + kept = lines.select do |line| + import_match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);}) + if import_match + package_name = import_match[1] + next true if package_name == "flet" + next true if selected_aliases.include?(import_match[2]) + next false + end + + extension_match = line.match(/\A\s*([a-zA-Z0-9_]+)\.Extension\(\),\s*\z/) + if extension_match + extension_alias = extension_match[1] + package_name = alias_to_package[extension_alias] + next true if package_name.nil? + next true if selected_aliases.include?(extension_alias) + next false + end + + true + end + + File.write(path, kept.join) end def update_pubspec_value(path, block, key, value, multiple: false) diff --git a/packages/ruflet_cli/lib/ruflet/cli/new_command.rb b/packages/ruflet_cli/lib/ruflet/cli/new_command.rb index 5c4fb8fa..dbdf3a4a 100644 --- a/packages/ruflet_cli/lib/ruflet/cli/new_command.rb +++ b/packages/ruflet_cli/lib/ruflet/cli/new_command.rb @@ -1,10 +1,30 @@ # frozen_string_literal: true require "fileutils" +require "yaml" module Ruflet module CLI module NewCommand + CLIENT_EXTENSION_MAP = { + "ads" => { package: "flet_ads", alias: "flet_ads" }, + "audio" => { package: "flet_audio", alias: "flet_audio" }, + "audio_recorder" => { package: "flet_audio_recorder", alias: "flet_audio_recorder" }, + "camera" => { package: "flet_camera", alias: "flet_camera" }, + "charts" => { package: "flet_charts", alias: "flet_charts" }, + "code_editor" => { package: "flet_code_editor", alias: "flet_code_editor" }, + "color_pickers" => { package: "flet_color_pickers", alias: "flet_color_picker" }, + "datatable2" => { package: "flet_datatable2", alias: "flet_datatable2" }, + "flashlight" => { package: "flet_flashlight", alias: "flet_flashlight" }, + "geolocator" => { package: "flet_geolocator", alias: "flet_geolocator" }, + "lottie" => { package: "flet_lottie", alias: "flet_lottie" }, + "map" => { package: "flet_map", alias: "flet_map" }, + "permission_handler" => { package: "flet_permission_handler", alias: "flet_permission_handler" }, + "secure_storage" => { package: "flet_secure_storage", alias: "flet_secure_storage" }, + "video" => { package: "flet_video", alias: "flet_video" }, + "webview" => { package: "flet_webview", alias: "flet_webview" } + }.freeze + def command_new(args) app_name = args.shift if app_name.nil? || app_name.strip.empty? @@ -22,7 +42,9 @@ def command_new(args) File.write(File.join(root, "main.rb"), format(Ruflet::CLI::MAIN_TEMPLATE, app_title: humanize_name(File.basename(root)))) File.write(File.join(root, "Gemfile"), Ruflet::CLI::GEMFILE_TEMPLATE) File.write(File.join(root, "README.md"), format(Ruflet::CLI::README_TEMPLATE, app_name: File.basename(root))) + write_default_ruflet_config(root, File.basename(root)) copy_ruflet_client_template(root) + configure_ruflet_client(root) project_name = File.basename(root) puts "Ruflet app created: #{project_name}" @@ -69,6 +91,129 @@ def prune_client_template(target) end end + def write_default_ruflet_config(root, app_name) + File.write(File.join(root, "ruflet.yaml"), <<~YAML) + app: + name: #{app_name} + # Optional production backend endpoint used by `ruflet build`. + # Example: https://api.example.com + backend_url: "" + + # Source of truth for Flutter client extensions/plugins. + # Examples: camera, video, audio, flashlight, webview, map + services: [] + + # Build assets configuration consumed by `ruflet build`. + # Paths are relative to this file unless absolute. + assets: + dir: assets + splash_screen: assets/splash.png + icon_launcher: assets/icon.png + + build: + splash_color: "#FFFFFF" + splash_dark_color: "#0B0B0B" + icon_background: "#FFFFFF" + theme_color: "#FFFFFF" + YAML + end + + def configure_ruflet_client(root) + config_path = File.join(root, "ruflet.yaml") + return unless File.file?(config_path) + + config = YAML.safe_load(File.read(config_path), aliases: true) || {} + extension_keys = extract_extension_keys(config) + extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq + extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq + + client_dir = File.join(root, "ruflet_client") + apply_client_manifest!(client_dir, extension_packages, extension_aliases) + rescue StandardError => e + warn "Failed to configure ruflet_client from ruflet.yaml: #{e.class}: #{e.message}" + end + + def extract_extension_keys(config) + from_services = Array(config["services"]) + + from_services + .map { |v| normalize_extension_key(v) } + .compact + .uniq + end + + def normalize_extension_key(value) + key = value.to_s.strip.downcase + return nil if key.empty? + + key.tr!("-", "_") + key.gsub!(/\A(flet_)+/, "") + key.gsub!(/\Aservice_/, "") + key.gsub!(/\Acontrol_/, "") + key = "file_picker" if key == "filepicker" + key + end + + def apply_client_manifest!(client_dir, extension_packages, extension_aliases) + return unless Dir.exist?(client_dir) + + pubspec_path = File.join(client_dir, "pubspec.yaml") + main_path = File.join(client_dir, "lib", "main.dart") + prune_client_pubspec(pubspec_path, extension_packages) if File.file?(pubspec_path) + prune_client_main(main_path, extension_aliases) if File.file?(main_path) + end + + def prune_client_pubspec(path, selected_packages) + data = YAML.safe_load(File.read(path), aliases: true) || {} + deps = (data["dependencies"] || {}).dup + + deps.keys.each do |name| + next unless name.start_with?("flet_") + next if name == "flet" + next if selected_packages.include?(name) + + deps.delete(name) + end + + data["dependencies"] = deps + File.write(path, YAML.dump(data)) + end + + def prune_client_main(path, selected_aliases) + lines = File.readlines(path) + alias_to_package = {} + + lines.each do |line| + match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);}) + next unless match + + alias_to_package[match[2]] = match[1] + end + + kept = lines.select do |line| + import_match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);}) + if import_match + package_name = import_match[1] + next true if package_name == "flet" + next true if selected_aliases.include?(import_match[2]) + next false + end + + extension_match = line.match(/\A\s*([a-zA-Z0-9_]+)\.Extension\(\),\s*\z/) + if extension_match + extension_alias = extension_match[1] + package_name = alias_to_package[extension_alias] + next true if package_name.nil? # non-Flet extension lines + next true if selected_aliases.include?(extension_alias) + next false + end + + true + end + + File.write(path, kept.join) + end + def humanize_name(name) name.to_s.gsub(/[_-]+/, " ").split.map(&:capitalize).join(" ") end diff --git a/packages/ruflet_cli/test/new_command_test.rb b/packages/ruflet_cli/test/new_command_test.rb index fe0fb9fe..9a1b6276 100644 --- a/packages/ruflet_cli/test/new_command_test.rb +++ b/packages/ruflet_cli/test/new_command_test.rb @@ -23,6 +23,7 @@ def test_command_new_creates_project_scaffold assert File.exist?(File.join(dir, "demo_app", "main.rb")) assert File.exist?(File.join(dir, "demo_app", "Gemfile")) assert File.exist?(File.join(dir, "demo_app", "README.md")) + assert File.exist?(File.join(dir, "demo_app", "ruflet.yaml")) refute File.exist?(File.join(dir, "demo_app", ".bundle", "config")) ensure $stdout = original_stdout @@ -36,4 +37,53 @@ def test_command_new_creates_project_scaffold end end end + + def test_prune_client_manifest_keeps_only_selected_extensions + Dir.mktmpdir do |dir| + client_dir = File.join(dir, "ruflet_client") + FileUtils.mkdir_p(File.join(client_dir, "lib")) + + File.write( + File.join(client_dir, "pubspec.yaml"), + <<~YAML + dependencies: + flutter: + sdk: flutter + flet: + git: + url: https://github.com/flet-dev/flet.git + flet_camera: + git: + url: https://github.com/flet-dev/flet.git + flet_video: + git: + url: https://github.com/flet-dev/flet.git + YAML + ) + + File.write( + File.join(client_dir, "lib", "main.dart"), + <<~DART + import 'package:flet/flet.dart'; + import 'package:flet_camera/flet_camera.dart' as flet_camera; + import 'package:flet_video/flet_video.dart' as flet_video; + + final extensions = [ + flet_camera.Extension(), + flet_video.Extension(), + ]; + DART + ) + + Ruflet::CLI.send(:apply_client_manifest!, client_dir, ["flet_camera"], ["flet_camera"]) + + pruned_pubspec = File.read(File.join(client_dir, "pubspec.yaml")) + pruned_main = File.read(File.join(client_dir, "lib", "main.dart")) + + assert_includes pruned_pubspec, "flet_camera:" + refute_includes pruned_pubspec, "flet_video:" + assert_includes pruned_main, "flet_camera.Extension()" + refute_includes pruned_main, "flet_video.Extension()" + end + end end diff --git a/templates/ruflet_flutter_template/lib/main.dart b/templates/ruflet_flutter_template/lib/main.dart index e12bdb86..bae9e2a3 100644 --- a/templates/ruflet_flutter_template/lib/main.dart +++ b/templates/ruflet_flutter_template/lib/main.dart @@ -35,6 +35,8 @@ import 'connection_probe.dart'; const bool isProduction = bool.fromEnvironment('dart.vm.product'); const int kRufletPort = 8550; +const String kConfiguredBackendUrl = + String.fromEnvironment('RUFLET_BACKEND_URL', defaultValue: ''); Tester? tester; String normalizePageUrlForPlatform(String rawUrl) { @@ -74,6 +76,12 @@ String normalizePageUrlForPlatform(String rawUrl) { String fallbackBackendUrl() => normalizePageUrlForPlatform('http://0.0.0.0:$kRufletPort'); +String resolveBackendUrl() { + final configured = parseBackendUrl(kConfiguredBackendUrl); + if (configured != null) return configured; + return fallbackBackendUrl(); +} + Future main() async { if (isProduction) { // ignore: avoid_returning_null_for_void @@ -116,7 +124,7 @@ Future main() async { extension.ensureInitialized(); } - final pageUrl = fallbackBackendUrl(); + final pageUrl = resolveBackendUrl(); await waitForBackend(pageUrl); From 0198adb0cf08ce46630dae8e8aecf4b5942918f9 Mon Sep 17 00:00:00 2001 From: AdamMusa Date: Mon, 9 Mar 2026 14:48:24 -0400 Subject: [PATCH 3/4] added webview support --- examples/calculator.rb | 213 ------------ examples/ruflet_studio/app.rb | 12 +- examples/ruflet_studio/helpers.rb | 40 ++- examples/ruflet_studio/icon_search.rb | 146 -------- examples/ruflet_studio/sections_charts.rb | 16 +- .../sections_controls/calculator.rb | 19 +- .../sections_controls/counter.rb | 54 ++- .../sections_controls/cupertino_controls.rb | 6 +- .../sections_controls/material_controls.rb | 10 +- .../ruflet_studio/sections_controls/todo.rb | 2 +- examples/ruflet_studio/sections_media.rb | 1 + .../ruflet_studio/sections_media/camera.rb | 96 ++++-- .../sections_media/file_picker.rb | 79 +++-- .../ruflet_studio/sections_media/webview.rb | 18 + .../ruflet_studio/sections_minesweeper.rb | 12 +- .../sections_misc/icon_search.rb | 54 +-- examples/ruflet_studio/views/detail_view.rb | 7 +- examples/ruflet_studio/views/gallery_view.rb | 14 +- examples/ruflet_studio/views/home_view.rb | 4 +- .../ruflet_studio/views/navigation_bar.rb | 4 +- examples/ruflet_studio/views/settings_view.rb | 38 ++- examples/ruflet_studio/views/status_text.rb | 4 +- packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb | 2 + packages/ruflet/lib/ruflet_ui/ruflet/page.rb | 26 +- .../ui/controls/materials/audio_control.rb | 36 ++ .../ui/controls/materials/chart_controls.rb | 321 ++++++++++++++++++ .../ui/controls/materials/ruflet_controls.rb | 44 +++ .../ui/controls/materials/webview_control.rb | 35 ++ .../ruflet/ui/controls/ruflet_controls.rb | 44 +++ .../ruflet/ui/material_control_methods.rb | 2 + .../ui/services/ruflet/camera_control.rb | 3 +- .../ui/services/ruflet/filepicker_control.rb | 3 +- .../ruflet/ui/shared_control_forwarders.rb | 2 + packages/ruflet/test/page_clipboard_test.rb | 6 +- .../lib/ruflet/cli/build_command.rb | 44 +-- .../ruflet_cli/lib/ruflet/cli/new_command.rb | 36 +- packages/ruflet_cli/test/new_command_test.rb | 14 +- packages/ruflet_server/lib/ruflet/server.rb | 21 +- .../ruflet/server/web_socket_connection.rb | 2 + .../ruflet_flutter_template/lib/main.dart | 70 ++-- 40 files changed, 916 insertions(+), 644 deletions(-) delete mode 100644 examples/calculator.rb delete mode 100644 examples/ruflet_studio/icon_search.rb create mode 100644 examples/ruflet_studio/sections_media/webview.rb create mode 100644 packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/audio_control.rb create mode 100644 packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/chart_controls.rb create mode 100644 packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/webview_control.rb diff --git a/examples/calculator.rb b/examples/calculator.rb deleted file mode 100644 index b62fb12f..00000000 --- a/examples/calculator.rb +++ /dev/null @@ -1,213 +0,0 @@ -require "ruflet" - -class CalculatorApp < Ruflet::App - DIGITS = %w[0 1 2 3 4 5 6 7 8 9].freeze - - def initialize - super - reset - end - - def view(page) - page.title = "Calculator" - page.bgcolor = "#000000" - - @display_control = text( - value: @display, - text_align: "right", - style: { size: 84, color: "#FFFFFF" } - ) - - page.add( - container( - expand: true, - bgcolor: "#000000", - padding: 12, - content: column( - expand: true, - spacing: 12, - children: [ - container(height: 24), - row(alignment: "end", children: [@display_control]), - - # pushes keypad toward bottom - container(expand: true), - - # gap between result and keyboard - container(height: 20), - - keypad_row(page, "BS", "AC", "%", "/"), - keypad_row(page, "7", "8", "9", "x"), - keypad_row(page, "4", "5", "6", "-"), - keypad_row(page, "1", "2", "3", "+"), - keypad_row(page, "+/-", "0", ".", "=") - ] - ) - ) - ) - end - - private - - def keypad_row(page, *labels) - row( - alignment: "center", - spacing: 10, - children: labels.map do |label| - elevated_button( - content: text(value: label), - expand: true, - height: 65, - color: "#FFFFFF", - bgcolor: key_bg(label), - on_click: ->(e) { handle_input(label, e) } - ) - end - ) - end - - def key_bg(label) - operator_label?(label) ? "#FF9F0A" : "#2C2C2E" - end - - def handle_input(label, event) - if DIGITS.include?(label) - on_digit(label) - elsif label == "." - on_decimal - elsif label == "x" - on_operator("x") - elsif label == "/" - on_operator("/") - elsif label == "-" - on_operator("-") - elsif label == "+" - on_operator("+") - elsif label == "=" - on_equals - elsif label == "AC" - reset - elsif label == "+/-" - on_toggle_sign - elsif label == "%" - on_percent - elsif label == "BS" - on_backspace - end - - event.page.update(@display_control, value: @display) - end - - def on_digit(digit) - if @start_new_value || @display == "Error" - @display = digit - @start_new_value = false - return - end - - @display = (@display == "0" ? digit : "#{@display}#{digit}") - end - - def on_decimal - if @start_new_value || @display == "Error" - @display = "0." - @start_new_value = false - return - end - - @display += "." unless @display.include?(".") - end - - def on_operator(next_operator) - if @operator && !@start_new_value - apply_calculation - return if @display == "Error" - else - @operand = to_number(@display) - end - - @operator = next_operator - @start_new_value = true - end - - def on_equals - return unless @operator - - apply_calculation - @operator = nil if @display != "Error" - end - - def on_toggle_sign - return if @display == "0" || @display == "Error" - - @display = @display.start_with?("-") ? @display[1..] : "-#{@display}" - end - - def on_percent - return if @display == "Error" - - @display = format_number(to_number(@display) / 100.0) - @start_new_value = true - end - - def on_backspace - return if @display == "Error" - - if @display.length <= 1 || (@display.length == 2 && @display.start_with?("-")) - @display = "0" - return - end - - @display = @display[0...-1] - end - - def apply_calculation - right = to_number(@display) - result = case @operator - when "+" then @operand + right - when "-" then @operand - right - when "x" then @operand * right - when "/" - return show_error if right.zero? - - @operand / right - end - - @display = format_number(result) - @operand = to_number(@display) - @start_new_value = true - end - - def to_number(value) - Float(value) - rescue StandardError - 0.0 - end - - def format_number(value) - value = value.to_f - return value.to_i.to_s if value == value.to_i - - value.to_s.sub(/\.?0+\z/, "") - end - - def show_error - @display = "Error" - @operator = nil - @operand = nil - @start_new_value = true - end - - def reset - @display = "0" - @operand = nil - @operator = nil - @start_new_value = false - end - - def operator_label?(label) - %w[/ x - + =].include?(label) - end -end - -CalculatorApp.new.run diff --git a/examples/ruflet_studio/app.rb b/examples/ruflet_studio/app.rb index 884b7553..c7e8cb12 100644 --- a/examples/ruflet_studio/app.rb +++ b/examples/ruflet_studio/app.rb @@ -24,7 +24,7 @@ class App < Ruflet::App def view(page) page.title = "Gallery" page.scroll = "auto" - page.bgcolor = "#111318" + page.bgcolor = color_bg(page) page.on_route_change = ->(_e) { render(page) } @@ -36,6 +36,7 @@ def view(page) def render(page) route = (page.route || "/gallery").split("?").first route = "/gallery" if route == "/" + page.bgcolor = color_bg(page) case route when "/home" @@ -46,7 +47,8 @@ def render(page) page.views = [settings_view(page)] when "/counter" page.views = [detail_view(page, "Counter", build_counter(page, status_text(page)), - source_path: "examples/ruflet_studio/sections_controls/counter.rb")] + source_path: "examples/ruflet_studio/sections_controls + /counter.rb")] when "/todo" page.views = [detail_view(page, "To-do", build_todo(page, status_text(page)), source_path: "examples/ruflet_studio/sections_controls/todo.rb")] @@ -80,6 +82,12 @@ def render(page) when "/video" page.views = [detail_view(page, "Video Player", build_video(page, status_text(page)), source_path: "examples/ruflet_studio/sections_media/video.rb")] + when "/webview" + page.views = [detail_view(page, "WebView", build_webview(page, status_text(page)), + source_path: "examples/ruflet_studio/sections_media/webview.rb", + scroll: nil, + horizontal_alignment: "stretch", + padding: 0)] when "/flashlight" page.views = [detail_view(page, "Flashlight", build_flashlight(page, status_text(page)), source_path: "examples/ruflet_studio/sections_media/flashlight.rb")] diff --git a/examples/ruflet_studio/helpers.rb b/examples/ruflet_studio/helpers.rb index aeeb496f..c84da349 100644 --- a/examples/ruflet_studio/helpers.rb +++ b/examples/ruflet_studio/helpers.rb @@ -60,7 +60,10 @@ def effective_theme(page) end def set_theme(page, mode) - @theme_mode = %w[system light dark].include?(mode) ? mode : "system" + normalized = mode.to_s.strip.downcase + return unless %w[system light dark].include?(normalized) + + @theme_mode = normalized page.go(page.route || "/settings") end @@ -72,16 +75,22 @@ def theme_colors(page) text: "#1f2328", subtle: "#6c757d", icon: "#495057", - divider: "#dee2e6" + divider: "#dee2e6", + panel: "#f1f3f5", + nav_indicator: "#dbe4ff", + accent: "#4c6ef5" } else { - bg: "#111318", - surface: "#111318", - text: "#e7e9ec", - subtle: "#9aa0a6", - icon: "#cfd4da", - divider: "#2a2e36" + bg: "#e8edf3", + surface: "#f8fafc", + text: "#1f2328", + subtle: "#5c6773", + icon: "#3f4954", + divider: "#cfd6de", + panel: "#eef2f6", + nav_indicator: "#cfe0ff", + accent: "#3b5bdb" } end end @@ -93,14 +102,18 @@ def color_text(page) = theme_colors(page)[:text] def color_subtle(page) = theme_colors(page)[:subtle] def color_icon(page) = theme_colors(page)[:icon] def color_divider(page) = theme_colors(page)[:divider] + def color_panel(page) = theme_colors(page)[:panel] + def color_nav_indicator(page) = theme_colors(page)[:nav_indicator] + def color_accent(page) = theme_colors(page)[:accent] def read_number(data, key) return nil unless data return data if data.is_a?(Numeric) return data.to_f if data.is_a?(String) && data.match?(/\A-?\d+(\.\d+)?\z/) - return data[key] if data.is_a?(Hash) && data[key].is_a?(Numeric) - if data.is_a?(Hash) && data[key] - return data[key].to_f + if data.is_a?(Hash) + raw = data[key] || data[key.to_s] || data[key.to_sym] + return raw if raw.is_a?(Numeric) + return raw.to_f if raw end nil end @@ -108,7 +121,10 @@ def read_number(data, key) def read_string(data, key) return nil unless data return data if data.is_a?(String) - return data[key] if data.is_a?(Hash) && data[key].is_a?(String) + if data.is_a?(Hash) + raw = data[key] || data[key.to_s] || data[key.to_sym] + return raw if raw.is_a?(String) + end nil end diff --git a/examples/ruflet_studio/icon_search.rb b/examples/ruflet_studio/icon_search.rb deleted file mode 100644 index 418914a3..00000000 --- a/examples/ruflet_studio/icon_search.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -require "ruflet" - -class IconSearchApp < Ruflet::App - MAX_RESULTS = 80 - - def initialize - super - @query = "" - @summary_control = nil - @results_grid = nil - @copy_status_control = nil - end - - def view(page) - page.title = "Icon Search" - render(page) - end - - private - - def render(page) - names = filtered_icon_names(@query) - @summary_control = text(value: summary_text(names), style: { size: 12, color: "#6c757d" }) - @copy_status_control = text(value: "Tap an item to copy icon name", style: { size: 12, color: "#6c757d" }) - @results_grid = build_results_grid(names) - - page.add( - container( - expand: true, - padding: 16, - alignment: Ruflet::MainAxisAlignment::CENTER, - content: column( - expand: true, - alignment: Ruflet::MainAxisAlignment::CENTER, - horizontal_alignment: Ruflet::CrossAxisAlignment::CENTER, - spacing: 12, - children: [ - text_field( - label: "Search Material icons", - value: @query, - autofocus: true, - on_change: ->(e) { - @query = event_value(e) - update_results(page) - } - ), - @summary_control, - @copy_status_control, - @results_grid - ] - ) - ), - appbar: app_bar( - title: text(value: "Icon Search") - ) - ) - end - - def update_results(page) - names = filtered_icon_names(@query) - page.update(@summary_control, value: summary_text(names)) - page.update(@results_grid, controls: grid_items(names)) - page.update(@copy_status_control, value: "Tap an item to copy icon name", style: { color: "#6c757d" }) - end - - def build_results_grid(names) - grid_view( - expand: true, - runs_count: 3, - max_extent: 220, - child_aspect_ratio: 2.0, - spacing: 10, - run_spacing: 10, - controls: grid_items(names) - ) - end - - def grid_items(names) - names.map { |name| icon_tile(name) } - end - - def icon_tile(name) - container( - padding: 10, - border_radius: 8, - on_click: ->(e) { copy_icon_name(e.page, name) }, - content: row( - spacing: 8, - children: [ - icon(icon: Ruflet::MaterialIcons.const_get(name)), - container( - expand: true, - content: text(value: name, max_lines: 1, ellipsis: true) - ) - ] - ) - ) - end - - def copy_icon_name(page, name) - call_id = page.set_clipboard(name) - if call_id - @copy_status_control.props["value"] = "Copied: #{name}" - puts "Copied to clipboard: #{name}" - @copy_status_control.props["color"] = "#2b8a3e" - else - @copy_status_control.props["value"] = "Copy failed: clipboard service unavailable" - @copy_status_control.props["color"] = "#c92a2a" - end - page.update(@copy_status_control, value: @copy_status_control.props["value"], style: { color: @copy_status_control.props["color"] }) - rescue StandardError => e - page.update(@copy_status_control, value: "Copy failed: #{e.message}", style: { color: "#c92a2a" }) - end - - def event_value(event) - data = event.data - return data if data.is_a?(String) - return data["value"].to_s if data.is_a?(Hash) && data["value"] - - "" - end - - def summary_text(names) - total = icon_names.size - shown = names.size - query = @query.to_s.strip - return "Type to search icons (#{total} available)" if query.empty? - - "Showing #{shown} results for \"#{query}\"" - end - - def icon_names - @icon_names ||= Ruflet::MaterialIcons.constants(false).map(&:to_s).sort - end - - def filtered_icon_names(query) - q = query.to_s.strip.upcase - return [] if q.empty? - - icon_names.select { |name| name.include?(q) }.first(MAX_RESULTS) - end -end - -IconSearchApp.new.run diff --git a/examples/ruflet_studio/sections_charts.rb b/examples/ruflet_studio/sections_charts.rb index 21f66521..4fbd050b 100644 --- a/examples/ruflet_studio/sections_charts.rb +++ b/examples/ruflet_studio/sections_charts.rb @@ -7,8 +7,8 @@ def build_charts(page, status) width: 320, height: 180, max_y: 110, - border: { width: 1, color: "#2a2e36" }, - horizontal_grid_lines: { color: "#2a2e36", width: 1, dash_pattern: [3, 3] }, + border: { width: 1, color: color_divider(page) }, + horizontal_grid_lines: { color: color_divider(page), width: 1, dash_pattern: [3, 3] }, tooltip: nil, left_axis: chart_axis(label_size: 40, title: text(value: "Fruit supply"), title_size: 40), right_axis: chart_axis(show_labels: false), @@ -125,17 +125,17 @@ def build_charts(page, status) spacing: 12, tight: true, children: [ - text(value: "BarChart", style: { size: 14, weight: "w600", color: "#e7e9ec" }), + text(value: "BarChart", style: { size: 14, weight: "w600" }), bar_chart, - text(value: "LineChart", style: { size: 14, weight: "w600", color: "#e7e9ec" }), + text(value: "LineChart", style: { size: 14, weight: "w600" }), line_chart, - text(value: "PieChart", style: { size: 14, weight: "w600", color: "#e7e9ec" }), + text(value: "PieChart", style: { size: 14, weight: "w600" }), pie_chart, - text(value: "CandlestickChart", style: { size: 14, weight: "w600", color: "#e7e9ec" }), + text(value: "CandlestickChart", style: { size: 14, weight: "w600" }), candlestick_chart, - text(value: "RadarChart", style: { size: 14, weight: "w600", color: "#e7e9ec" }), + text(value: "RadarChart", style: { size: 14, weight: "w600" }), radar_chart, - text(value: "ScatterChart", style: { size: 14, weight: "w600", color: "#e7e9ec" }), + text(value: "ScatterChart", style: { size: 14, weight: "w600" }), scatter_chart ] ) diff --git a/examples/ruflet_studio/sections_controls/calculator.rb b/examples/ruflet_studio/sections_controls/calculator.rb index ecd640ff..e3aa1d48 100644 --- a/examples/ruflet_studio/sections_controls/calculator.rb +++ b/examples/ruflet_studio/sections_controls/calculator.rb @@ -6,15 +6,16 @@ module SectionsControls def build_calculator(page, status) container( - expand: true, + width: 420, padding: 12, + border_radius: 12, + bgcolor: color_panel(page), content: column( - expand: true, spacing: 12, children: [ + status, container(height: 24), row(alignment: "end", children: [calculator_display(status)]), - container(expand: true), container(height: 20), calculator_keypad_row(page, status, "BS", "AC", "%", "/"), calculator_keypad_row(page, status, "7", "8", "9", "x"), @@ -34,29 +35,29 @@ def calculator_display(_status) @calculator_display = text( value: calculator_state[:display], text_align: "right", - style: { size: 84, color: "#FFFFFF" } + style: { size: 84 } ) end def calculator_keypad_row(page, status, *labels) row( alignment: "center", - spacing: 10, + spacing: 6, children: labels.map do |label| elevated_button( content: text(value: label), - expand: true, + width: 78, height: 65, color: "#FFFFFF", - bgcolor: calculator_key_bg(label), + bgcolor: calculator_key_bg(page, label), on_click: ->(e) { calculator_handle_input(label, e, page, status) } ) end ) end - def calculator_key_bg(label) - %w[/ x - + =].include?(label) ? "#FF9F0A" : "#2C2C2E" + def calculator_key_bg(page, label) + %w[/ x - + =].include?(label) ? color_accent(page) : color_surface(page) end def calculator_handle_input(label, event, page, status) diff --git a/examples/ruflet_studio/sections_controls/counter.rb b/examples/ruflet_studio/sections_controls/counter.rb index a8cf295e..96389dab 100644 --- a/examples/ruflet_studio/sections_controls/counter.rb +++ b/examples/ruflet_studio/sections_controls/counter.rb @@ -4,24 +4,44 @@ module RufletStudio module SectionsControls def build_counter(page, status) count = 0 - value = text_field(value: count.to_s, text_align: "right", width: 80) + value = text(value: count.to_s, style: { size: 28 }) - row( - spacing: 8, - alignment: "center", - children: [ - icon_button(icon: "remove", on_click: ->(_e) { - count -= 1 - page.update(value, value: count.to_s) - page.update(status, value: "Counter: #{count}") - }), - value, - icon_button(icon: "add", on_click: ->(_e) { - count += 1 - page.update(value, value: count.to_s) - page.update(status, value: "Counter: #{count}") - }) - ] + container( + width: 320, + padding: 12, + border_radius: 12, + bgcolor: color_panel(page), + content: column( + spacing: 12, + children: [ + status, + row(alignment: "center", children: [value]), + row( + alignment: "center", + spacing: 10, + children: [ + elevated_button( + width: 120, + content: text(value: "-1"), + on_click: ->(_e) { + count -= 1 + page.update(value, value: count.to_s) + page.update(status, value: "Counter: #{count}") + } + ), + elevated_button( + width: 120, + content: text(value: "+1"), + on_click: ->(_e) { + count += 1 + page.update(value, value: count.to_s) + page.update(status, value: "Counter: #{count}") + } + ) + ] + ), + ] + ) ) end end diff --git a/examples/ruflet_studio/sections_controls/cupertino_controls.rb b/examples/ruflet_studio/sections_controls/cupertino_controls.rb index 248557dc..8681966d 100644 --- a/examples/ruflet_studio/sections_controls/cupertino_controls.rb +++ b/examples/ruflet_studio/sections_controls/cupertino_controls.rb @@ -18,9 +18,9 @@ def build_cupertino_controls(page, status) use_magnifier: true, item_extent: 32, children: [ - text(value: "One", style: { color: "#111318" }), - text(value: "Two", style: { color: "#111318" }), - text(value: "Three", style: { color: "#111318" }) + text(value: "One"), + text(value: "Two"), + text(value: "Three") ] ) diff --git a/examples/ruflet_studio/sections_controls/material_controls.rb b/examples/ruflet_studio/sections_controls/material_controls.rb index bba1d165..c9f55533 100644 --- a/examples/ruflet_studio/sections_controls/material_controls.rb +++ b/examples/ruflet_studio/sections_controls/material_controls.rb @@ -36,7 +36,7 @@ def build_material_controls(page, status) content: column( spacing: 8, children: [ - text(value: "TextField", style: { size: 14, weight: "w600", color: "#1f2328" }), + text(value: "TextField", style: { size: 14, weight: "w600" }), text_field(label: "Name", value: "Ruflet") ] ) @@ -49,7 +49,7 @@ def build_material_controls(page, status) content: column( spacing: 8, children: [ - text(value: "Buttons", style: { size: 14, weight: "w600", color: "#1f2328" }), + text(value: "Buttons", style: { size: 14, weight: "w600" }), row( spacing: 8, children: [ @@ -69,7 +69,7 @@ def build_material_controls(page, status) content: column( spacing: 8, children: [ - text(value: "Selection", style: { size: 14, weight: "w600", color: "#1f2328" }), + text(value: "Selection", style: { size: 14, weight: "w600" }), control(:switch, label: "Wi-Fi", value: true), control(:slider, min: 0, max: 100, divisions: 10, value: 35, label: "Value = {value}") ] @@ -83,7 +83,7 @@ def build_material_controls(page, status) content: column( spacing: 8, children: [ - text(value: "Dialogs", style: { size: 14, weight: "w600", color: "#1f2328" }), + text(value: "Dialogs", style: { size: 14, weight: "w600" }), text_button(content: text(value: "Show dialog"), on_click: ->(_e) { page.show_dialog(material_dialog) }) ] ) @@ -96,7 +96,7 @@ def build_material_controls(page, status) content: column( spacing: 8, children: [ - text(value: "Banners", style: { size: 14, weight: "w600", color: "#1f2328" }), + text(value: "Banners", style: { size: 14, weight: "w600" }), text_button(content: text(value: "Show banner"), on_click: ->(_e) { page.show_dialog(build_banner.call) }) diff --git a/examples/ruflet_studio/sections_controls/todo.rb b/examples/ruflet_studio/sections_controls/todo.rb index 722267d8..41d23274 100644 --- a/examples/ruflet_studio/sections_controls/todo.rb +++ b/examples/ruflet_studio/sections_controls/todo.rb @@ -65,7 +65,7 @@ def build_todo(page, _status) column( spacing: 8, children: [ - text(value: "Todos", style: { size: 20, weight: "w600", color: "#e7e9ec" }), + text(value: "Todos", style: { size: 20, weight: "w600" }), input, button(content: text(value: "Add"), on_click: ->(_e) { add_todo.call }), list, diff --git a/examples/ruflet_studio/sections_media.rb b/examples/ruflet_studio/sections_media.rb index 03224af2..b360b749 100644 --- a/examples/ruflet_studio/sections_media.rb +++ b/examples/ruflet_studio/sections_media.rb @@ -6,6 +6,7 @@ require_relative "sections_media/flashlight" require_relative "sections_media/camera" require_relative "sections_media/file_picker" +require_relative "sections_media/webview" module RufletStudio module SectionsMedia diff --git a/examples/ruflet_studio/sections_media/camera.rb b/examples/ruflet_studio/sections_media/camera.rb index 211089e1..4d6baff5 100644 --- a/examples/ruflet_studio/sections_media/camera.rb +++ b/examples/ruflet_studio/sections_media/camera.rb @@ -6,58 +6,80 @@ def build_camera(page, status) camera = page.service( :camera, preview_enabled: true, - expand: true, on_error: ->(e) { page.update(status, value: "Camera error: #{e.data}") } ) + camera_busy = false + open_button = nil preview = container( visible: false, height: 320, border_radius: 10, - bgcolor: "#000000", + bgcolor: color_panel(page), border: { width: 1, color: color_divider(page) }, content: camera ) + open_button = button( + content: text(value: "Open camera"), + on_click: ->(_e) do + next if camera_busy + camera_busy = true + page.update(open_button, disabled: true) + page.update(status, value: "Checking available cameras...") + page.invoke( + camera, + "get_available_cameras", + timeout: 45, + on_result: lambda { |result, error| + if error && !error.to_s.empty? + camera_busy = false + page.update(open_button, disabled: false) + page.update(status, value: "Camera error: #{error}") + next + end + + cameras = Array(result) + if cameras.empty? + camera_busy = false + page.update(open_button, disabled: false) + page.update(status, value: "No camera available on this device.") + next + end + + page.update(status, value: "Initializing camera...") + page.invoke( + camera, + "initialize", + args: { + "description" => cameras.first, + "resolution_preset" => "medium", + "enable_audio" => false, + "image_format_group" => "jpeg" + }, + timeout: 180, + on_result: lambda { |_init_result, init_error| + camera_busy = false + page.update(open_button, disabled: false) + if init_error && !init_error.to_s.empty? + page.update(status, value: "Camera error: #{init_error}") + else + page.update(preview, visible: true) + page.update(status, value: "Camera initialized.") + end + } + ) + } + ) + end + ) + column( spacing: 10, children: [ status, - button( - content: text(value: "Open camera"), - on_click: ->(_e) do - page.update(status, value: "Checking available cameras...") - page.invoke(camera, "get_available_cameras", timeout: 30, on_result: lambda { |result, error| - if error - page.update(status, value: "Camera error: #{error}") - next - end - - cameras = Array(result) - if cameras.empty? - page.update(status, value: "No camera available on this device.") - next - end - - page.update(status, value: "Initializing camera...") - page.invoke( - camera, - "initialize", - args: { "description" => cameras.first }, - timeout: 60, - on_result: lambda { |_init_result, init_error| - if init_error - page.update(status, value: "Camera init error: #{init_error}") - else - page.update(preview, visible: true) - page.update(status, value: "Camera ready.") - end - } - ) - }) - end - ), - text(value: "Tap Open camera to initialize and show preview.", style: { size: 12, color: color_subtle(page) }), + open_button, + text(value: "Tap Open camera to initialize and show preview.", style: { size: 12 }), preview ] ) diff --git a/examples/ruflet_studio/sections_media/file_picker.rb b/examples/ruflet_studio/sections_media/file_picker.rb index 403460bd..574562de 100644 --- a/examples/ruflet_studio/sections_media/file_picker.rb +++ b/examples/ruflet_studio/sections_media/file_picker.rb @@ -1,53 +1,52 @@ # frozen_string_literal: true -require "json" - module RufletStudio module SectionsMedia def build_file_picker(page, status) - file_picker = page.service(:file_picker) + file_picker = page.service(:filepicker) + picker_busy = false + open_button = nil + + open_button = button( + content: text(value: "Open file picker"), + on_click: ->(_e) do + next if picker_busy + picker_busy = true + page.update(open_button, disabled: true) + page.update(status, value: "Opening file picker...") + page.invoke( + file_picker, + "pick_files", + args: { "allow_multiple" => false, "with_data" => false }, + timeout: 600, + on_result: lambda { |result, error| + picker_busy = false + page.update(open_button, disabled: false) + + if error && !error.to_s.empty? + page.update(status, value: "File picker error: #{error}") + next + end + + files = Array(result) + if files.empty? + page.update(status, value: "No file selected.") + else + first = files.first || {} + name = first["name"] || first[:name] || "unknown" + path = first["path"] || first[:path] || "no-path" + page.update(status, value: "Selected: #{name} (#{path})") + end + } + ) + end + ) column( spacing: 10, children: [ status, - button( - content: text(value: "Open file picker"), - on_click: ->(_e) do - page.update(status, value: "Opening file picker...") - page.invoke( - file_picker, - "pick_files", - args: { "allow_multiple" => false, "with_data" => false }, - timeout: 600, - on_result: lambda { |result, error| - if error - page.update(status, value: "File picker error: #{error}") - next - end - - files = Array(result) - if files.empty? - page.update(status, value: "No file selected.") - next - end - - first = files.first || {} - if first.is_a?(String) - begin - first = JSON.parse(first) - rescue StandardError - first = { "name" => first, "path" => nil } - end - end - - name = first["name"] || first[:name] || "unknown" - path = first["path"] || first[:path] || "no-path" - page.update(status, value: "Selected: #{name} (#{path})") - } - ) - end - ) + open_button ] ) end diff --git a/examples/ruflet_studio/sections_media/webview.rb b/examples/ruflet_studio/sections_media/webview.rb new file mode 100644 index 00000000..4eec6a36 --- /dev/null +++ b/examples/ruflet_studio/sections_media/webview.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RufletStudio + module SectionsMedia + def build_webview(_page, _status) + webview_control = web_view( + url: "https://flet.dev", + method: "get", + expand: true + ) + + container( + expand: true, + content: webview_control + ) + end + end +end diff --git a/examples/ruflet_studio/sections_minesweeper.rb b/examples/ruflet_studio/sections_minesweeper.rb index c85332fd..82777be2 100644 --- a/examples/ruflet_studio/sections_minesweeper.rb +++ b/examples/ruflet_studio/sections_minesweeper.rb @@ -47,9 +47,9 @@ def build_minesweeper(page, status) end mines_left = mine_count - mines_text = text(value: format("%03d", mines_left), style: { color: "#ff6b6b", size: 16, weight: "w600" }) + mines_text = text(value: format("%03d", mines_left), style: { size: 16, weight: "w600" }) face_text = text(value: "🙂", style: { size: 18 }) - timer_text = text(value: "000", style: { color: "#ff6b6b", size: 16, weight: "w600" }) + timer_text = text(value: "000", style: { size: 16, weight: "w600" }) cell_texts = [] cell_containers = [] @@ -68,7 +68,7 @@ def build_minesweeper(page, status) end label = sq[:flagged] ? "🚩" : "" - label_text = text(value: label, style: { size: 14, color: number_color }) + label_text = text(value: label, style: { size: 14 }) cell_texts[idx] = label_text cell_containers[idx] = container( @@ -150,7 +150,7 @@ def build_minesweeper(page, status) } end }) - safe_update.call(text, { value: label, style: { color: number_color } }) + safe_update.call(text, { value: label }) end safe_update.call(mines_text, { value: format("%03d", mines_left) }) @@ -375,8 +375,8 @@ def build_minesweeper_grid(page) height: size, left: c * size, top: r * size, - bgcolor: r.even? == c.even? ? "#2a2e36" : "#23272f", - border: { width: 1, color: "#1c1f26" } + bgcolor: r.even? == c.even? ? "#e9ecef" : "#dee2e6", + border: { width: 1, color: "#ced4da" } ) end end diff --git a/examples/ruflet_studio/sections_misc/icon_search.rb b/examples/ruflet_studio/sections_misc/icon_search.rb index d4147611..3f7a4cf1 100644 --- a/examples/ruflet_studio/sections_misc/icon_search.rb +++ b/examples/ruflet_studio/sections_misc/icon_search.rb @@ -3,13 +3,13 @@ module RufletStudio module SectionsMisc ICON_SEARCH_MAX_RESULTS = 80 + ICON_SEARCH_RESULTS_HEIGHT = 420 def build_icon_search(page, status) query = "" - summary = text(value: icon_search_summary_text(query, []), style: { size: 12, color: color_subtle(page) }) - copy_status = text(value: "Tap an item to copy icon name", style: { size: 12, color: color_subtle(page) }) + summary = text(value: icon_search_summary_text(query, []), style: { size: 12 }) + copy_status = text(value: "Tap an item to copy icon name", style: { size: 12 }) results_grid = grid_view( - expand: true, runs_count: 3, max_extent: 220, child_aspect_ratio: 2.0, @@ -23,27 +23,33 @@ def build_icon_search(page, status) names = icon_search_filtered_names(query) page.update(summary, value: icon_search_summary_text(query, names)) page.update(results_grid, controls: names.map { |name| icon_search_tile(page, name, copy_status) }) - page.update(copy_status, value: "Tap an item to copy icon name", style: { color: color_subtle(page) }) + page.update(copy_status, value: "Tap an item to copy icon name") end - column( - spacing: 10, - children: [ - status, - text_field( - label: "Search Material icons", - autofocus: true, - value: query, - on_change: ->(e) { - data = e.data - value = data.is_a?(Hash) ? (data["value"] || data[:value]) : data - on_query_change.call(value.to_s) - } - ), - summary, - copy_status, - results_grid - ] + container( + width: 760, + content: column( + spacing: 10, + children: [ + status, + text_field( + label: "Search Material icons", + autofocus: true, + value: query, + on_change: ->(e) { + data = e.data + value = data.is_a?(Hash) ? (data["value"] || data[:value]) : data + on_query_change.call(value.to_s) + } + ), + summary, + copy_status, + container( + height: ICON_SEARCH_RESULTS_HEIGHT, + content: results_grid + ) + ] + ) ) end @@ -54,9 +60,9 @@ def icon_search_tile(page, name, copy_status) on_click: ->(_e) { call_id = page.set_clipboard(name) if call_id - page.update(copy_status, value: "Copied: #{name}", style: { color: "#2b8a3e" }) + page.update(copy_status, value: "Copied: #{name}") else - page.update(copy_status, value: "Copy failed: clipboard service unavailable", style: { color: "#c92a2a" }) + page.update(copy_status, value: "Copy failed: clipboard service unavailable") end }, content: row( diff --git a/examples/ruflet_studio/views/detail_view.rb b/examples/ruflet_studio/views/detail_view.rb index 2e93bb5b..5de0d559 100644 --- a/examples/ruflet_studio/views/detail_view.rb +++ b/examples/ruflet_studio/views/detail_view.rb @@ -2,12 +2,13 @@ module RufletStudio module Views - def detail_view(page, title, content, source_path: nil) + def detail_view(page, title, content, source_path: nil, scroll: "auto", horizontal_alignment: "center", padding: 16) route = page.route control(:view, route: route, bgcolor: color_bg(page), - scroll: "auto", + scroll: scroll, + horizontal_alignment: horizontal_alignment, appbar: app_bar( bgcolor: color_surface(page), color: color_text(page), @@ -22,7 +23,7 @@ def detail_view(page, title, content, source_path: nil) end ), navigation_bar: nav_bar(page, "/gallery"), - padding: 16, + padding: padding, children: [ content ] diff --git a/examples/ruflet_studio/views/gallery_view.rb b/examples/ruflet_studio/views/gallery_view.rb index c1762f82..7137a26f 100644 --- a/examples/ruflet_studio/views/gallery_view.rb +++ b/examples/ruflet_studio/views/gallery_view.rb @@ -17,9 +17,12 @@ def gallery_view(page) ), navigation_bar: nav_bar(page, route), children: [ - column( - spacing: 6, - children: gallery_items(page) + container( + padding: { top: 12, left: 0, right: 0, bottom: 8 }, + content: column( + spacing: 6, + children: gallery_items(page) + ) ) ] ) @@ -31,6 +34,7 @@ def gallery_items(page) tile(page, "check", "To-do", "/todo"), tile(page, "calculate", "Calculator", "/calculator"), tile(page, "brush", "Drawing Tool", "/drawing"), + tile(page, "public", "WebView", "/webview"), tile(page, "view_module", "Material controls", "/material"), tile(page, "phone_iphone", "Cupertino controls", "/cupertino"), tile(page, "show_chart", "Charts", "/charts"), @@ -48,8 +52,10 @@ def gallery_items(page) def tile(page, icon, title, route) control( :list_tile, + bgcolor: color_surface(page), + content_padding: { left: 12, right: 12, top: 8, bottom: 8 }, leading: icon(icon: icon, color: color_icon(page)), - title: text(value: title, style: { color: color_text(page), size: 16 }), + title: text(value: title, style: { size: 16 }), trailing: icon(icon: "chevron_right", color: color_subtle(page)), on_click: ->(_e) { page.go(route) } ) diff --git a/examples/ruflet_studio/views/home_view.rb b/examples/ruflet_studio/views/home_view.rb index 157bbc5b..3cdc07df 100644 --- a/examples/ruflet_studio/views/home_view.rb +++ b/examples/ruflet_studio/views/home_view.rb @@ -16,8 +16,8 @@ def home_view(page) navigation_bar: nav_bar(page, route), padding: 16, children: [ - text(value: "Home", style: { size: 18, color: color_text(page) }), - text(value: "Use the Gallery tab to explore controls.", style: { color: color_subtle(page) }) + text(value: "Home", style: { size: 18 }), + text(value: "Use the Gallery tab to explore controls.") ] ) end diff --git a/examples/ruflet_studio/views/navigation_bar.rb b/examples/ruflet_studio/views/navigation_bar.rb index 01b0c627..2105fd03 100644 --- a/examples/ruflet_studio/views/navigation_bar.rb +++ b/examples/ruflet_studio/views/navigation_bar.rb @@ -11,10 +11,12 @@ def nav_bar(page, route) navigation_bar( bgcolor: color_surface(page), - indicator_color: effective_theme(page) == "light" ? "#dbe4ff" : "#2b3036", + indicator_color: color_nav_indicator(page), selected_index: selected, on_change: ->(e) { idx = read_number(e.data, "selected_index") || read_number(e.data, "selectedIndex") + next if idx.nil? || idx.to_i == selected + case idx&.to_i when 0 page.go("/home") diff --git a/examples/ruflet_studio/views/settings_view.rb b/examples/ruflet_studio/views/settings_view.rb index 9160bf2f..25f1f742 100644 --- a/examples/ruflet_studio/views/settings_view.rb +++ b/examples/ruflet_studio/views/settings_view.rb @@ -24,14 +24,18 @@ def settings_view(page) column( spacing: 16, children: [ - text(value: "Theme", style: { size: 14, color: color_icon(page) }), + text(value: "Theme", style: { size: 14 }), radio_group( value: theme_mode, on_change: ->(e) { - value = read_string(e.data, "value") || read_string(e.data, "selected") || e.data.to_s + value = + read_string(e.data, "value") || + read_string(e.data, :value) || + read_string(e.data, "selected") || + read_string(e.data, :selected) + next unless %w[system light dark].include?(value) + set_theme(page, value) - page.views = [settings_view(page)] - page.update }, content: column( spacing: 14, @@ -43,7 +47,7 @@ def settings_view(page) spacing: 12, children: [ icon(icon: "contrast", color: color_icon(page)), - text(value: "System", style: { color: color_text(page) }) + text(value: "System") ] ), radio(value: "system") @@ -56,7 +60,7 @@ def settings_view(page) spacing: 12, children: [ icon(icon: "light_mode", color: color_icon(page)), - text(value: "Light", style: { color: color_text(page) }) + text(value: "Light") ] ), radio(value: "light") @@ -69,7 +73,7 @@ def settings_view(page) spacing: 12, children: [ icon(icon: "dark_mode", color: color_icon(page)), - text(value: "Dark", style: { color: color_text(page) }) + text(value: "Dark") ] ), radio(value: "dark") @@ -79,11 +83,11 @@ def settings_view(page) ) ), container(height: 1, bgcolor: color_divider(page), margin: { top: 8, bottom: 8 }), - text(value: "Home gestures", style: { size: 14, color: color_icon(page) }), + text(value: "Home gestures", style: { size: 14 }), control( :list_tile, leading: icon(icon: "vibration", color: color_icon(page)), - title: text(value: "Shake device", style: { color: color_text(page) }), + title: text(value: "Shake device"), trailing: gestures_shake, on_click: ->(_e) { gestures_shake_state = !gestures_shake_state @@ -93,7 +97,7 @@ def settings_view(page) control( :list_tile, leading: icon(icon: "pan_tool_alt", color: color_icon(page)), - title: text(value: "Long press with two fingers", style: { color: color_text(page) }), + title: text(value: "Long press with two fingers"), trailing: gestures_long_press, on_click: ->(_e) { gestures_long_press_state = !gestures_long_press_state @@ -101,26 +105,26 @@ def settings_view(page) } ), container(height: 1, bgcolor: color_divider(page), margin: { top: 8, bottom: 8 }), - text(value: "Application details", style: { size: 14, color: color_icon(page) }), + text(value: "Application details", style: { size: 14 }), row( alignment: "spaceBetween", children: [ - text(value: "Client version:", style: { color: color_text(page) }), - text(value: "#{Ruflet::VERSION}", style: { color: color_subtle(page) }) + text(value: "Client version:"), + text(value: "#{Ruflet::VERSION}") ] ), row( alignment: "spaceBetween", children: [ - text(value: "Ruflet SDK version:", style: { color: color_text(page) }), - text(value: "#{Ruflet::VERSION}", style: { color: color_subtle(page) }) + text(value: "Ruflet SDK version:"), + text(value: "#{Ruflet::VERSION}") ] ), row( alignment: "spaceBetween", children: [ - text(value: "Ruby version:", style: { color: color_text(page) }), - text(value: "#{RUBY_VERSION}", style: { color: color_subtle(page) }) + text(value: "Ruby version:"), + text(value: "#{RUBY_VERSION}") ] ) ] diff --git a/examples/ruflet_studio/views/status_text.rb b/examples/ruflet_studio/views/status_text.rb index 95af2f25..ff85437d 100644 --- a/examples/ruflet_studio/views/status_text.rb +++ b/examples/ruflet_studio/views/status_text.rb @@ -2,8 +2,8 @@ module RufletStudio module Views - def status_text(page) - text(value: "Ready", style: { size: 12, color: color_subtle(page) }) + def status_text(_page) + text(value: "", style: { size: 12 }) end end end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb b/packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb index 416b7bde..a7a1bb63 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/dsl.rb @@ -138,6 +138,8 @@ def chart_axis(**props) = _pending_app.chart_axis(**props) def chartaxis(**props) = _pending_app.chartaxis(**props) def chart_axis_label(**props) = _pending_app.chart_axis_label(**props) def chartaxislabel(**props) = _pending_app.chartaxislabel(**props) + def web_view(**props) = _pending_app.web_view(**props) + def webview(**props) = _pending_app.webview(**props) def fab(content = nil, **props) = _pending_app.fab(content, **props) def cupertino_button(**props) = _pending_app.cupertino_button(**props) def cupertinobutton(**props) = _pending_app.cupertinobutton(**props) diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/page.rb b/packages/ruflet/lib/ruflet_ui/ruflet/page.rb index 0d6dc7b4..ee48f003 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/page.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/page.rb @@ -284,9 +284,10 @@ def invoke_sync(control_or_id, method_name, args: nil, timeout: 10) end def launch_url(url, mode: "external_application", web_view_configuration: nil, browser_configuration: nil, web_only_window_name: nil, timeout: 10) + url_launcher = ensure_url_launcher_service invoke( - 1, - "launchUrl", + url_launcher, + "launch_url", args: { "url" => url, "mode" => mode, @@ -299,7 +300,8 @@ def launch_url(url, mode: "external_application", web_view_configuration: nil, b end def can_launch_url(url, timeout: 10) - invoke(1, "canLaunchUrl", args: { "url" => url }, timeout: timeout) + url_launcher = ensure_url_launcher_service + invoke(url_launcher, "can_launch_url", args: { "url" => url }, timeout: timeout) end def set_clipboard(value, timeout: 10) @@ -374,6 +376,13 @@ def update(control_or_id = nil, **props) control = resolve_control(control_or_id) return self unless control + wire_id = control.wire_id + if wire_id.nil? + # Events can race with navigation/disposal; never emit patch_control with nil id. + refresh_control_indexes! + wire_id = control.wire_id + end + return self if wire_id.nil? patch = normalize_props(props) if text_maps_to_content?(control, patch) @@ -392,7 +401,7 @@ def update(control_or_id = nil, **props) patch_ops = patch.map { |k, v| [0, 0, k, serialize_patch_value(v)] } send_message(Protocol::ACTIONS[:patch_control], { - "id" => control.wire_id, + "id" => wire_id, "patch" => [[0], *patch_ops] }) @@ -781,5 +790,14 @@ def ensure_clipboard_service add_service(clipboard) clipboard end + + def ensure_url_launcher_service + url_launcher = services.find { |service| service.is_a?(Control) && %w[urllauncher url_launcher].include?(service.type) } + return url_launcher if url_launcher + + url_launcher = build_widget(:url_launcher) + add_service(url_launcher) + url_launcher + end end end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/audio_control.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/audio_control.rb new file mode 100644 index 00000000..0e82cb56 --- /dev/null +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/audio_control.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Ruflet + module UI + module Controls + module RufletComponents + class AudioControl < Ruflet::Control + TYPE = "audio".freeze + WIRE = "Audio".freeze + + def initialize(id: nil, autoplay: nil, balance: nil, data: nil, key: nil, opacity: nil, release_mode: nil, rtl: nil, src: nil, tooltip: nil, visible: nil, volume: nil, on_duration_change: nil, on_error: nil, on_loaded: nil, on_position_change: nil, on_seek_complete: nil, on_state_change: nil) + props = {} + props[:autoplay] = autoplay unless autoplay.nil? + props[:balance] = balance unless balance.nil? + props[:data] = data unless data.nil? + props[:key] = key unless key.nil? + props[:opacity] = opacity unless opacity.nil? + props[:release_mode] = release_mode unless release_mode.nil? + props[:rtl] = rtl unless rtl.nil? + props[:src] = src unless src.nil? + props[:tooltip] = tooltip unless tooltip.nil? + props[:visible] = visible unless visible.nil? + props[:volume] = volume unless volume.nil? + props[:on_duration_change] = on_duration_change unless on_duration_change.nil? + props[:on_error] = on_error unless on_error.nil? + props[:on_loaded] = on_loaded unless on_loaded.nil? + props[:on_position_change] = on_position_change unless on_position_change.nil? + props[:on_seek_complete] = on_seek_complete unless on_seek_complete.nil? + props[:on_state_change] = on_state_change unless on_state_change.nil? + super(type: TYPE, id: id, **props) + end + end + end + end + end +end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/chart_controls.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/chart_controls.rb new file mode 100644 index 00000000..cd9d1c5e --- /dev/null +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/chart_controls.rb @@ -0,0 +1,321 @@ +# frozen_string_literal: true + +module Ruflet + module UI + module Controls + module RufletComponents + class ChartAxisControl < Ruflet::Control + TYPE = "chartaxis".freeze + WIRE = "axis".freeze + + def initialize(id: nil, title: nil, labels: nil, label_size: nil, title_size: nil, show_labels: nil) + props = {} + props[:title] = title unless title.nil? + props[:labels] = labels unless labels.nil? + props[:label_size] = label_size unless label_size.nil? + props[:title_size] = title_size unless title_size.nil? + props[:show_labels] = show_labels unless show_labels.nil? + super(type: TYPE, id: id, **props) + end + end + + class ChartAxisLabelControl < Ruflet::Control + TYPE = "chartaxislabel".freeze + WIRE = "l".freeze + + def initialize(id: nil, value: nil, label: nil) + props = {} + props[:value] = value unless value.nil? + props[:label] = label unless label.nil? + super(type: TYPE, id: id, **props) + end + end + + class BarChartControl < Ruflet::Control + TYPE = "barchart".freeze + WIRE = "BarChart".freeze + + def initialize(id: nil, width: nil, height: nil, min_y: nil, max_y: nil, min_x: nil, max_x: nil, groups: nil, left_axis: nil, right_axis: nil, top_axis: nil, bottom_axis: nil, horizontal_grid_lines: nil, vertical_grid_lines: nil, border: nil, tooltip: nil, on_event: nil) + props = {} + props[:width] = width unless width.nil? + props[:height] = height unless height.nil? + props[:min_y] = min_y unless min_y.nil? + props[:max_y] = max_y unless max_y.nil? + props[:min_x] = min_x unless min_x.nil? + props[:max_x] = max_x unless max_x.nil? + props[:groups] = groups unless groups.nil? + props[:left_axis] = left_axis unless left_axis.nil? + props[:right_axis] = right_axis unless right_axis.nil? + props[:top_axis] = top_axis unless top_axis.nil? + props[:bottom_axis] = bottom_axis unless bottom_axis.nil? + props[:horizontal_grid_lines] = horizontal_grid_lines unless horizontal_grid_lines.nil? + props[:vertical_grid_lines] = vertical_grid_lines unless vertical_grid_lines.nil? + props[:border] = border unless border.nil? + props[:tooltip] = tooltip unless tooltip.nil? + props[:on_event] = on_event unless on_event.nil? + super(type: TYPE, id: id, **props) + end + end + + class BarChartGroupControl < Ruflet::Control + TYPE = "barchartgroup".freeze + WIRE = "group".freeze + + def initialize(id: nil, x: nil, rods: nil, bars_space: nil, showing_tooltip_indicators: nil) + props = {} + props[:x] = x unless x.nil? + props[:rods] = rods unless rods.nil? + props[:bars_space] = bars_space unless bars_space.nil? + props[:showing_tooltip_indicators] = showing_tooltip_indicators unless showing_tooltip_indicators.nil? + super(type: TYPE, id: id, **props) + end + end + + class BarChartRodControl < Ruflet::Control + TYPE = "barchartrod".freeze + WIRE = "rod".freeze + + def initialize(id: nil, from_y: nil, to_y: nil, width: nil, color: nil, gradient: nil, border_radius: nil, rod_stack_items: nil) + props = {} + props[:from_y] = from_y unless from_y.nil? + props[:to_y] = to_y unless to_y.nil? + props[:width] = width unless width.nil? + props[:color] = color unless color.nil? + props[:gradient] = gradient unless gradient.nil? + props[:border_radius] = border_radius unless border_radius.nil? + props[:rod_stack_items] = rod_stack_items unless rod_stack_items.nil? + super(type: TYPE, id: id, **props) + end + end + + class BarChartRodStackItemControl < Ruflet::Control + TYPE = "barchartrodstackitem".freeze + WIRE = "stack_item".freeze + + def initialize(id: nil, from_y: nil, to_y: nil, color: nil, border_side: nil) + props = {} + props[:from_y] = from_y unless from_y.nil? + props[:to_y] = to_y unless to_y.nil? + props[:color] = color unless color.nil? + props[:border_side] = border_side unless border_side.nil? + super(type: TYPE, id: id, **props) + end + end + + class LineChartControl < Ruflet::Control + TYPE = "linechart".freeze + WIRE = "LineChart".freeze + + def initialize(id: nil, width: nil, height: nil, min_y: nil, max_y: nil, min_x: nil, max_x: nil, data_series: nil, left_axis: nil, right_axis: nil, top_axis: nil, bottom_axis: nil, interactive: nil, tooltip: nil, on_event: nil) + props = {} + props[:width] = width unless width.nil? + props[:height] = height unless height.nil? + props[:min_y] = min_y unless min_y.nil? + props[:max_y] = max_y unless max_y.nil? + props[:min_x] = min_x unless min_x.nil? + props[:max_x] = max_x unless max_x.nil? + props[:data_series] = data_series unless data_series.nil? + props[:left_axis] = left_axis unless left_axis.nil? + props[:right_axis] = right_axis unless right_axis.nil? + props[:top_axis] = top_axis unless top_axis.nil? + props[:bottom_axis] = bottom_axis unless bottom_axis.nil? + props[:interactive] = interactive unless interactive.nil? + props[:tooltip] = tooltip unless tooltip.nil? + props[:on_event] = on_event unless on_event.nil? + super(type: TYPE, id: id, **props) + end + end + + class LineChartDataControl < Ruflet::Control + TYPE = "linechartdata".freeze + WIRE = "data".freeze + + def initialize(id: nil, points: nil, color: nil, gradient: nil, stroke_width: nil, curved: nil, rounded_stroke_cap: nil) + props = {} + props[:points] = points unless points.nil? + props[:color] = color unless color.nil? + props[:gradient] = gradient unless gradient.nil? + props[:stroke_width] = stroke_width unless stroke_width.nil? + props[:curved] = curved unless curved.nil? + props[:rounded_stroke_cap] = rounded_stroke_cap unless rounded_stroke_cap.nil? + super(type: TYPE, id: id, **props) + end + end + + class LineChartDataPointControl < Ruflet::Control + TYPE = "linechartdatapoint".freeze + WIRE = "p".freeze + + def initialize(id: nil, x: nil, y: nil) + props = {} + props[:x] = x unless x.nil? + props[:y] = y unless y.nil? + super(type: TYPE, id: id, **props) + end + end + + class PieChartControl < Ruflet::Control + TYPE = "piechart".freeze + WIRE = "PieChart".freeze + + def initialize(id: nil, width: nil, height: nil, sections: nil, sections_space: nil, center_space_radius: nil, tooltip: nil, on_event: nil) + props = {} + props[:width] = width unless width.nil? + props[:height] = height unless height.nil? + props[:sections] = sections unless sections.nil? + props[:sections_space] = sections_space unless sections_space.nil? + props[:center_space_radius] = center_space_radius unless center_space_radius.nil? + props[:tooltip] = tooltip unless tooltip.nil? + props[:on_event] = on_event unless on_event.nil? + super(type: TYPE, id: id, **props) + end + end + + class PieChartSectionControl < Ruflet::Control + TYPE = "piechartsection".freeze + WIRE = "section".freeze + + def initialize(id: nil, value: nil, title: nil, color: nil, radius: nil, title_style: nil, badge_widget: nil, badge_position_percentage_offset: nil) + props = {} + props[:value] = value unless value.nil? + props[:title] = title unless title.nil? + props[:color] = color unless color.nil? + props[:radius] = radius unless radius.nil? + props[:title_style] = title_style unless title_style.nil? + props[:badge_widget] = badge_widget unless badge_widget.nil? + props[:badge_position_percentage_offset] = badge_position_percentage_offset unless badge_position_percentage_offset.nil? + super(type: TYPE, id: id, **props) + end + end + + class CandlestickChartControl < Ruflet::Control + TYPE = "candlestickchart".freeze + WIRE = "CandlestickChart".freeze + + def initialize(id: nil, width: nil, height: nil, min_x: nil, max_x: nil, min_y: nil, max_y: nil, spots: nil, left_axis: nil, right_axis: nil, top_axis: nil, bottom_axis: nil, tooltip: nil, on_event: nil) + props = {} + props[:width] = width unless width.nil? + props[:height] = height unless height.nil? + props[:min_x] = min_x unless min_x.nil? + props[:max_x] = max_x unless max_x.nil? + props[:min_y] = min_y unless min_y.nil? + props[:max_y] = max_y unless max_y.nil? + props[:spots] = spots unless spots.nil? + props[:left_axis] = left_axis unless left_axis.nil? + props[:right_axis] = right_axis unless right_axis.nil? + props[:top_axis] = top_axis unless top_axis.nil? + props[:bottom_axis] = bottom_axis unless bottom_axis.nil? + props[:tooltip] = tooltip unless tooltip.nil? + props[:on_event] = on_event unless on_event.nil? + super(type: TYPE, id: id, **props) + end + end + + class CandlestickChartSpotControl < Ruflet::Control + TYPE = "candlestickchartspot".freeze + WIRE = "CandlestickChartSpot".freeze + + def initialize(id: nil, x: nil, open: nil, high: nil, low: nil, close: nil, selected: nil) + props = {} + props[:x] = x unless x.nil? + props[:open] = open unless open.nil? + props[:high] = high unless high.nil? + props[:low] = low unless low.nil? + props[:close] = close unless close.nil? + props[:selected] = selected unless selected.nil? + super(type: TYPE, id: id, **props) + end + end + + class RadarChartControl < Ruflet::Control + TYPE = "radarchart".freeze + WIRE = "RadarChart".freeze + + def initialize(id: nil, width: nil, height: nil, titles: nil, data_sets: nil, on_event: nil) + props = {} + props[:width] = width unless width.nil? + props[:height] = height unless height.nil? + props[:titles] = titles unless titles.nil? + props[:data_sets] = data_sets unless data_sets.nil? + props[:on_event] = on_event unless on_event.nil? + super(type: TYPE, id: id, **props) + end + end + + class RadarChartTitleControl < Ruflet::Control + TYPE = "radarcharttitle".freeze + WIRE = "RadarChartTitle".freeze + + def initialize(id: nil, text: nil, angle: nil, position_percentage_offset: nil) + props = {} + props[:text] = text unless text.nil? + props[:angle] = angle unless angle.nil? + props[:position_percentage_offset] = position_percentage_offset unless position_percentage_offset.nil? + super(type: TYPE, id: id, **props) + end + end + + class RadarDataSetControl < Ruflet::Control + TYPE = "radardataset".freeze + WIRE = "RadarDataSet".freeze + + def initialize(id: nil, entries: nil, border_color: nil, fill_color: nil, border_width: nil) + props = {} + props[:entries] = entries unless entries.nil? + props[:border_color] = border_color unless border_color.nil? + props[:fill_color] = fill_color unless fill_color.nil? + props[:border_width] = border_width unless border_width.nil? + super(type: TYPE, id: id, **props) + end + end + + class RadarDataSetEntryControl < Ruflet::Control + TYPE = "radardatasetentry".freeze + WIRE = "RadarDataSetEntry".freeze + + def initialize(id: nil, value: nil) + props = {} + props[:value] = value unless value.nil? + super(type: TYPE, id: id, **props) + end + end + + class ScatterChartControl < Ruflet::Control + TYPE = "scatterchart".freeze + WIRE = "ScatterChart".freeze + + def initialize(id: nil, width: nil, height: nil, min_x: nil, max_x: nil, min_y: nil, max_y: nil, spots: nil, left_axis: nil, right_axis: nil, top_axis: nil, bottom_axis: nil, on_event: nil) + props = {} + props[:width] = width unless width.nil? + props[:height] = height unless height.nil? + props[:min_x] = min_x unless min_x.nil? + props[:max_x] = max_x unless max_x.nil? + props[:min_y] = min_y unless min_y.nil? + props[:max_y] = max_y unless max_y.nil? + props[:spots] = spots unless spots.nil? + props[:left_axis] = left_axis unless left_axis.nil? + props[:right_axis] = right_axis unless right_axis.nil? + props[:top_axis] = top_axis unless top_axis.nil? + props[:bottom_axis] = bottom_axis unless bottom_axis.nil? + props[:on_event] = on_event unless on_event.nil? + super(type: TYPE, id: id, **props) + end + end + + class ScatterChartSpotControl < Ruflet::Control + TYPE = "scatterchartspot".freeze + WIRE = "ScatterChartSpot".freeze + + def initialize(id: nil, x: nil, y: nil, radius: nil, color: nil) + props = {} + props[:x] = x unless x.nil? + props[:y] = y unless y.nil? + props[:radius] = radius unless radius.nil? + props[:color] = color unless color.nil? + super(type: TYPE, id: id, **props) + end + end + end + end + end +end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/ruflet_controls.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/ruflet_controls.rb index 56ea78c8..7e503717 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/ruflet_controls.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/ruflet_controls.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require_relative "alertdialog_control" +require_relative "audio_control" require_relative "appbar_control" require_relative "autocomplete_control" require_relative "badge_control" require_relative "banner_control" +require_relative "chart_controls" require_relative "bottomappbar_control" require_relative "bottomsheet_control" require_relative "button_control" @@ -69,6 +71,7 @@ require_relative "textfield_control" require_relative "timepicker_control" require_relative "verticaldivider_control" +require_relative "webview_control" module Ruflet module UI @@ -80,6 +83,7 @@ module RufletControls CLASS_MAP = { "alert_dialog" => RufletComponents::AlertDialogControl, "alertdialog" => RufletComponents::AlertDialogControl, + "audio" => RufletComponents::AudioControl, "app_bar" => RufletComponents::AppBarControl, "appbar" => RufletComponents::AppBarControl, "auto_complete" => RufletComponents::AutoCompleteControl, @@ -90,8 +94,24 @@ module RufletControls "bottom_sheet" => RufletComponents::BottomSheetControl, "bottomappbar" => RufletComponents::BottomAppBarControl, "bottomsheet" => RufletComponents::BottomSheetControl, + "bar_chart" => RufletComponents::BarChartControl, + "bar_chart_group" => RufletComponents::BarChartGroupControl, + "bar_chart_rod" => RufletComponents::BarChartRodControl, + "bar_chart_rod_stack_item" => RufletComponents::BarChartRodStackItemControl, + "barchart" => RufletComponents::BarChartControl, + "barchartgroup" => RufletComponents::BarChartGroupControl, + "barchartrod" => RufletComponents::BarChartRodControl, + "barchartrodstackitem" => RufletComponents::BarChartRodStackItemControl, "button" => RufletComponents::ButtonControl, "card" => RufletComponents::CardControl, + "candlestick_chart" => RufletComponents::CandlestickChartControl, + "candlestick_chart_spot" => RufletComponents::CandlestickChartSpotControl, + "candlestickchart" => RufletComponents::CandlestickChartControl, + "candlestickchartspot" => RufletComponents::CandlestickChartSpotControl, + "chart_axis" => RufletComponents::ChartAxisControl, + "chart_axis_label" => RufletComponents::ChartAxisLabelControl, + "chartaxis" => RufletComponents::ChartAxisControl, + "chartaxislabel" => RufletComponents::ChartAxisLabelControl, "checkbox" => RufletComponents::CheckboxControl, "chip" => RufletComponents::ChipControl, "circle_avatar" => RufletComponents::CircleAvatarControl, @@ -135,6 +155,12 @@ module RufletControls "floatingactionbutton" => RufletComponents::FloatingActionButtonControl, "icon_button" => RufletComponents::IconButtonControl, "iconbutton" => RufletComponents::IconButtonControl, + "line_chart" => RufletComponents::LineChartControl, + "line_chart_data" => RufletComponents::LineChartDataControl, + "line_chart_data_point" => RufletComponents::LineChartDataPointControl, + "linechart" => RufletComponents::LineChartControl, + "linechartdata" => RufletComponents::LineChartDataControl, + "linechartdatapoint" => RufletComponents::LineChartDataPointControl, "list_tile" => RufletComponents::ListTileControl, "listtile" => RufletComponents::ListTileControl, "menu_bar" => RufletComponents::MenuBarControl, @@ -162,6 +188,10 @@ module RufletControls "popup_menu_item" => RufletComponents::PopupMenuItemControl, "popupmenubutton" => RufletComponents::PopupMenuButtonControl, "popupmenuitem" => RufletComponents::PopupMenuItemControl, + "pie_chart" => RufletComponents::PieChartControl, + "pie_chart_section" => RufletComponents::PieChartSectionControl, + "piechart" => RufletComponents::PieChartControl, + "piechartsection" => RufletComponents::PieChartSectionControl, "progress_bar" => RufletComponents::ProgressBarControl, "progress_ring" => RufletComponents::ProgressRingControl, "progressbar" => RufletComponents::ProgressBarControl, @@ -169,6 +199,14 @@ module RufletControls "radio" => RufletComponents::RadioControl, "radio_group" => RufletComponents::RadioGroupControl, "radiogroup" => RufletComponents::RadioGroupControl, + "radar_chart" => RufletComponents::RadarChartControl, + "radar_chart_title" => RufletComponents::RadarChartTitleControl, + "radar_data_set" => RufletComponents::RadarDataSetControl, + "radar_data_set_entry" => RufletComponents::RadarDataSetEntryControl, + "radarchart" => RufletComponents::RadarChartControl, + "radarcharttitle" => RufletComponents::RadarChartTitleControl, + "radardataset" => RufletComponents::RadarDataSetControl, + "radardatasetentry" => RufletComponents::RadarDataSetEntryControl, "range_slider" => RufletComponents::RangeSliderControl, "rangeslider" => RufletComponents::RangeSliderControl, "reorderable_list_view" => RufletComponents::ReorderableListViewControl, @@ -180,6 +218,10 @@ module RufletControls "segmentedbutton" => RufletComponents::SegmentedButtonControl, "selection_area" => RufletComponents::SelectionAreaControl, "selectionarea" => RufletComponents::SelectionAreaControl, + "scatter_chart" => RufletComponents::ScatterChartControl, + "scatter_chart_spot" => RufletComponents::ScatterChartSpotControl, + "scatterchart" => RufletComponents::ScatterChartControl, + "scatterchartspot" => RufletComponents::ScatterChartSpotControl, "slider" => RufletComponents::SliderControl, "snack_bar" => RufletComponents::SnackBarControl, "snackbar" => RufletComponents::SnackBarControl, @@ -200,6 +242,8 @@ module RufletControls "timepicker" => RufletComponents::TimePickerControl, "vertical_divider" => RufletComponents::VerticalDividerControl, "verticaldivider" => RufletComponents::VerticalDividerControl, + "web_view" => RufletComponents::WebViewControl, + "webview" => RufletComponents::WebViewControl, }.freeze end end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/webview_control.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/webview_control.rb new file mode 100644 index 00000000..8145072e --- /dev/null +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/materials/webview_control.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Ruflet + module UI + module Controls + module RufletComponents + class WebViewControl < Ruflet::Control + TYPE = "WebView".freeze + WIRE = "WebView".freeze + + def initialize(id: nil, bgcolor: nil, data: nil, enable_javascript: nil, expand: nil, height: nil, key: nil, method: nil, opacity: nil, rtl: nil, tooltip: nil, url: nil, visible: nil, width: nil, on_page_ended: nil, on_page_started: nil, on_web_resource_error: nil) + props = {} + props[:bgcolor] = bgcolor unless bgcolor.nil? + props[:data] = data unless data.nil? + props[:enable_javascript] = enable_javascript unless enable_javascript.nil? + props[:expand] = expand unless expand.nil? + props[:height] = height unless height.nil? + props[:key] = key unless key.nil? + props[:method] = method unless method.nil? + props[:opacity] = opacity unless opacity.nil? + props[:rtl] = rtl unless rtl.nil? + props[:tooltip] = tooltip unless tooltip.nil? + props[:url] = url unless url.nil? + props[:visible] = visible unless visible.nil? + props[:width] = width unless width.nil? + props[:on_page_ended] = on_page_ended unless on_page_ended.nil? + props[:on_page_started] = on_page_started unless on_page_started.nil? + props[:on_web_resource_error] = on_web_resource_error unless on_web_resource_error.nil? + super(type: TYPE, id: id, **props) + end + end + end + end + end +end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/ruflet_controls.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/ruflet_controls.rb index 7d74f131..906f68d4 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/ruflet_controls.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/controls/ruflet_controls.rb @@ -25,10 +25,12 @@ require_relative "cupertinos/cupertinotimerpicker_control" require_relative "cupertinos/cupertinotintedbutton_control" require_relative "materials/alertdialog_control" +require_relative "materials/audio_control" require_relative "materials/appbar_control" require_relative "materials/autocomplete_control" require_relative "materials/badge_control" require_relative "materials/banner_control" +require_relative "materials/chart_controls" require_relative "materials/bottomappbar_control" require_relative "materials/bottomsheet_control" require_relative "materials/button_control" @@ -94,6 +96,7 @@ require_relative "materials/timepicker_control" require_relative "materials/verticaldivider_control" require_relative "materials/video_control" +require_relative "materials/webview_control" require_relative "shared/animatedswitcher_control" require_relative "shared/arc_control" require_relative "shared/autofillgroup_control" @@ -155,6 +158,7 @@ module RufletControls CLASS_MAP = { "alert_dialog" => RufletComponents::AlertDialogControl, "alertdialog" => RufletComponents::AlertDialogControl, + "audio" => RufletComponents::AudioControl, "animated_switcher" => RufletComponents::AnimatedSwitcherControl, "animatedswitcher" => RufletComponents::AnimatedSwitcherControl, "app_bar" => RufletComponents::AppBarControl, @@ -172,11 +176,27 @@ module RufletControls "bottom_sheet" => RufletComponents::BottomSheetControl, "bottomappbar" => RufletComponents::BottomAppBarControl, "bottomsheet" => RufletComponents::BottomSheetControl, + "bar_chart" => RufletComponents::BarChartControl, + "bar_chart_group" => RufletComponents::BarChartGroupControl, + "bar_chart_rod" => RufletComponents::BarChartRodControl, + "bar_chart_rod_stack_item" => RufletComponents::BarChartRodStackItemControl, + "barchart" => RufletComponents::BarChartControl, + "barchartgroup" => RufletComponents::BarChartGroupControl, + "barchartrod" => RufletComponents::BarChartRodControl, + "barchartrodstackitem" => RufletComponents::BarChartRodStackItemControl, "browser_context_menu" => RufletComponents::BrowserContextMenuControl, "browsercontextmenu" => RufletComponents::BrowserContextMenuControl, "button" => RufletComponents::ButtonControl, "canvas" => RufletComponents::CanvasControl, "card" => RufletComponents::CardControl, + "candlestick_chart" => RufletComponents::CandlestickChartControl, + "candlestick_chart_spot" => RufletComponents::CandlestickChartSpotControl, + "candlestickchart" => RufletComponents::CandlestickChartControl, + "candlestickchartspot" => RufletComponents::CandlestickChartSpotControl, + "chart_axis" => RufletComponents::ChartAxisControl, + "chart_axis_label" => RufletComponents::ChartAxisLabelControl, + "chartaxis" => RufletComponents::ChartAxisControl, + "chartaxislabel" => RufletComponents::ChartAxisLabelControl, "checkbox" => RufletComponents::CheckboxControl, "chip" => RufletComponents::ChipControl, "circle" => RufletComponents::CircleControl, @@ -291,6 +311,12 @@ module RufletControls "keyboard_listener" => RufletComponents::KeyboardListenerControl, "keyboardlistener" => RufletComponents::KeyboardListenerControl, "line" => RufletComponents::LineControl, + "line_chart" => RufletComponents::LineChartControl, + "line_chart_data" => RufletComponents::LineChartDataControl, + "line_chart_data_point" => RufletComponents::LineChartDataPointControl, + "linechart" => RufletComponents::LineChartControl, + "linechartdata" => RufletComponents::LineChartDataControl, + "linechartdatapoint" => RufletComponents::LineChartDataPointControl, "list_tile" => RufletComponents::ListTileControl, "list_view" => RufletComponents::ListViewControl, "listtile" => RufletComponents::ListTileControl, @@ -332,6 +358,10 @@ module RufletControls "popup_menu_item" => RufletComponents::PopupMenuItemControl, "popupmenubutton" => RufletComponents::PopupMenuButtonControl, "popupmenuitem" => RufletComponents::PopupMenuItemControl, + "pie_chart" => RufletComponents::PieChartControl, + "pie_chart_section" => RufletComponents::PieChartSectionControl, + "piechart" => RufletComponents::PieChartControl, + "piechartsection" => RufletComponents::PieChartSectionControl, "progress_bar" => RufletComponents::ProgressBarControl, "progress_ring" => RufletComponents::ProgressRingControl, "progressbar" => RufletComponents::ProgressBarControl, @@ -339,6 +369,14 @@ module RufletControls "radio" => RufletComponents::RadioControl, "radio_group" => RufletComponents::RadioGroupControl, "radiogroup" => RufletComponents::RadioGroupControl, + "radar_chart" => RufletComponents::RadarChartControl, + "radar_chart_title" => RufletComponents::RadarChartTitleControl, + "radar_data_set" => RufletComponents::RadarDataSetControl, + "radar_data_set_entry" => RufletComponents::RadarDataSetEntryControl, + "radarchart" => RufletComponents::RadarChartControl, + "radarcharttitle" => RufletComponents::RadarChartTitleControl, + "radardataset" => RufletComponents::RadarDataSetControl, + "radardatasetentry" => RufletComponents::RadarDataSetEntryControl, "range_slider" => RufletComponents::RangeSliderControl, "rangeslider" => RufletComponents::RangeSliderControl, "rect" => RufletComponents::RectControl, @@ -358,6 +396,10 @@ module RufletControls "segmentedbutton" => RufletComponents::SegmentedButtonControl, "selection_area" => RufletComponents::SelectionAreaControl, "selectionarea" => RufletComponents::SelectionAreaControl, + "scatter_chart" => RufletComponents::ScatterChartControl, + "scatter_chart_spot" => RufletComponents::ScatterChartSpotControl, + "scatterchart" => RufletComponents::ScatterChartControl, + "scatterchartspot" => RufletComponents::ScatterChartSpotControl, "semantics" => RufletComponents::SemanticsControl, "service_registry" => RufletComponents::ServiceRegistryControl, "serviceregistry" => RufletComponents::ServiceRegistryControl, @@ -392,6 +434,8 @@ module RufletControls "vertical_divider" => RufletComponents::VerticalDividerControl, "verticaldivider" => RufletComponents::VerticalDividerControl, "video" => RufletComponents::VideoControl, + "web_view" => RufletComponents::WebViewControl, + "webview" => RufletComponents::WebViewControl, "view" => RufletComponents::ViewControl, "window" => RufletComponents::WindowControl, "window_drag_area" => RufletComponents::WindowDragAreaControl, diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/material_control_methods.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/material_control_methods.rb index aab4b14b..9be08d49 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/material_control_methods.rb @@ -136,6 +136,8 @@ def chart_axis(**props) = build_widget(:chartaxis, **props) def chartaxis(**props) = chart_axis(**props) def chart_axis_label(**props) = build_widget(:chartaxislabel, **props) def chartaxislabel(**props) = chart_axis_label(**props) + def web_view(**props) = build_widget(:webview, **props) + def webview(**props) = web_view(**props) def fab(content = nil, **props) mapped = props.dup diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/services/ruflet/camera_control.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/services/ruflet/camera_control.rb index a2cb421d..e26c7bd2 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/ui/services/ruflet/camera_control.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/services/ruflet/camera_control.rb @@ -8,7 +8,7 @@ class CameraControl < Ruflet::Control TYPE = "camera".freeze WIRE = "Camera".freeze - def initialize(id: nil, align: nil, animate_align: nil, animate_margin: nil, animate_offset: nil, animate_opacity: nil, animate_position: nil, animate_rotation: nil, animate_scale: nil, animate_size: nil, aspect_ratio: nil, badge: nil, bottom: nil, col: nil, content: nil, data: nil, disabled: nil, expand: nil, expand_loose: nil, height: nil, key: nil, left: nil, margin: nil, offset: nil, opacity: nil, preview_enabled: nil, right: nil, rotate: nil, rtl: nil, scale: nil, size_change_interval: nil, tooltip: nil, top: nil, visible: nil, width: nil, on_animation_end: nil, on_size_change: nil, on_state_change: nil, on_stream_image: nil) + def initialize(id: nil, align: nil, animate_align: nil, animate_margin: nil, animate_offset: nil, animate_opacity: nil, animate_position: nil, animate_rotation: nil, animate_scale: nil, animate_size: nil, aspect_ratio: nil, badge: nil, bottom: nil, col: nil, content: nil, data: nil, disabled: nil, expand: nil, expand_loose: nil, height: nil, key: nil, left: nil, margin: nil, offset: nil, opacity: nil, preview_enabled: nil, right: nil, rotate: nil, rtl: nil, scale: nil, size_change_interval: nil, tooltip: nil, top: nil, visible: nil, width: nil, on_animation_end: nil, on_error: nil, on_size_change: nil, on_state_change: nil, on_stream_image: nil) props = {} props[:align] = align unless align.nil? props[:animate_align] = animate_align unless animate_align.nil? @@ -45,6 +45,7 @@ def initialize(id: nil, align: nil, animate_align: nil, animate_margin: nil, ani props[:visible] = visible unless visible.nil? props[:width] = width unless width.nil? props[:on_animation_end] = on_animation_end unless on_animation_end.nil? + props[:on_error] = on_error unless on_error.nil? props[:on_size_change] = on_size_change unless on_size_change.nil? props[:on_state_change] = on_state_change unless on_state_change.nil? props[:on_stream_image] = on_stream_image unless on_stream_image.nil? diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/services/ruflet/filepicker_control.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/services/ruflet/filepicker_control.rb index 2205e884..b6e7fd15 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/ui/services/ruflet/filepicker_control.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/services/ruflet/filepicker_control.rb @@ -8,10 +8,11 @@ class FilePickerControl < Ruflet::Control TYPE = "filepicker".freeze WIRE = "FilePicker".freeze - def initialize(id: nil, data: nil, key: nil, on_upload: nil) + def initialize(id: nil, data: nil, key: nil, on_result: nil, on_upload: nil) props = {} props[:data] = data unless data.nil? props[:key] = key unless key.nil? + props[:on_result] = on_result unless on_result.nil? props[:on_upload] = on_upload unless on_upload.nil? super(type: TYPE, id: id, **props) end diff --git a/packages/ruflet/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb b/packages/ruflet/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb index 669f7d74..f3230686 100644 --- a/packages/ruflet/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +++ b/packages/ruflet/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb @@ -97,6 +97,8 @@ def chart_axis(**props) = control_delegate.chart_axis(**props) def chartaxis(**props) = control_delegate.chartaxis(**props) def chart_axis_label(**props) = control_delegate.chart_axis_label(**props) def chartaxislabel(**props) = control_delegate.chartaxislabel(**props) + def web_view(**props) = control_delegate.web_view(**props) + def webview(**props) = control_delegate.webview(**props) def cupertino_button(**props) = control_delegate.cupertino_button(**props) def cupertinobutton(**props) = control_delegate.cupertinobutton(**props) def cupertino_filled_button(**props) = control_delegate.cupertino_filled_button(**props) diff --git a/packages/ruflet/test/page_clipboard_test.rb b/packages/ruflet/test/page_clipboard_test.rb index 8c7155b1..55d6a515 100644 --- a/packages/ruflet/test/page_clipboard_test.rb +++ b/packages/ruflet/test/page_clipboard_test.rb @@ -35,7 +35,7 @@ def test_set_clipboard_image_uses_data_key assert_equal({ "data" => "abc123" }, invoke_payload["args"]) end - def test_launch_url_uses_page_invoke_signature + def test_launch_url_uses_url_launcher_service_signature sent = [] page = Ruflet::Page.new( session_id: "s1", @@ -46,9 +46,9 @@ def test_launch_url_uses_page_invoke_signature call_id = page.launch_url("https://flet.dev") refute_nil call_id - invoke_payload = sent.reverse.map(&:last).find { |payload| payload["name"] == "launchUrl" } + invoke_payload = sent.reverse.map(&:last).find { |payload| payload["name"] == "launch_url" } refute_nil invoke_payload - assert_equal 1, invoke_payload["control_id"] + refute_equal 1, invoke_payload["control_id"] assert_equal "https://flet.dev", invoke_payload.dig("args", "url") end end diff --git a/packages/ruflet_cli/lib/ruflet/cli/build_command.rb b/packages/ruflet_cli/lib/ruflet/cli/build_command.rb index 6babed3e..cabc1aee 100644 --- a/packages/ruflet_cli/lib/ruflet/cli/build_command.rb +++ b/packages/ruflet_cli/lib/ruflet/cli/build_command.rb @@ -9,22 +9,22 @@ module CLI module BuildCommand include FlutterSdk CLIENT_EXTENSION_MAP = { - "ads" => { package: "flet_ads", alias: "flet_ads" }, - "audio" => { package: "flet_audio", alias: "flet_audio" }, - "audio_recorder" => { package: "flet_audio_recorder", alias: "flet_audio_recorder" }, - "camera" => { package: "flet_camera", alias: "flet_camera" }, - "charts" => { package: "flet_charts", alias: "flet_charts" }, - "code_editor" => { package: "flet_code_editor", alias: "flet_code_editor" }, - "color_pickers" => { package: "flet_color_pickers", alias: "flet_color_picker" }, - "datatable2" => { package: "flet_datatable2", alias: "flet_datatable2" }, - "flashlight" => { package: "flet_flashlight", alias: "flet_flashlight" }, - "geolocator" => { package: "flet_geolocator", alias: "flet_geolocator" }, - "lottie" => { package: "flet_lottie", alias: "flet_lottie" }, - "map" => { package: "flet_map", alias: "flet_map" }, - "permission_handler" => { package: "flet_permission_handler", alias: "flet_permission_handler" }, - "secure_storage" => { package: "flet_secure_storage", alias: "flet_secure_storage" }, - "video" => { package: "flet_video", alias: "flet_video" }, - "webview" => { package: "flet_webview", alias: "flet_webview" } + "ads" => { package: "flet_ads", alias: "ruflet_ads" }, + "audio" => { package: "flet_audio", alias: "ruflet_audio" }, + "audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" }, + "camera" => { package: "flet_camera", alias: "ruflet_camera" }, + "charts" => { package: "flet_charts", alias: "ruflet_charts" }, + "code_editor" => { package: "flet_code_editor", alias: "ruflet_code_editor" }, + "color_pickers" => { package: "flet_color_pickers", alias: "ruflet_color_picker" }, + "datatable2" => { package: "flet_datatable2", alias: "ruflet_datatable2" }, + "flashlight" => { package: "flet_flashlight", alias: "ruflet_flashlight" }, + "geolocator" => { package: "flet_geolocator", alias: "ruflet_geolocator" }, + "lottie" => { package: "flet_lottie", alias: "ruflet_lottie" }, + "map" => { package: "flet_map", alias: "ruflet_map" }, + "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" }, + "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" }, + "video" => { package: "flet_video", alias: "ruflet_video" }, + "webview" => { package: "flet_webview", alias: "ruflet_webview" } }.freeze def command_build(args) @@ -53,9 +53,9 @@ def command_build(args) return 1 unless ok build_args = [*flutter_cmd, *args] - backend_url = configured_backend_url(config) - if backend_url - build_args += ["--dart-define", "RUFLET_BACKEND_URL=#{backend_url}"] + client_url = configured_client_url(config) + if client_url + build_args += ["--dart-define", "RUFLET_CLIENT_URL=#{client_url}"] end ok = system(tools[:env], tools[:flutter], *build_args, chdir: client_dir) @@ -106,10 +106,10 @@ def prepare_flutter_client(client_dir, tools:, config:) true end - def configured_backend_url(config) + def configured_client_url(config) candidates = [ - config["backend_url"], - (config["app"].is_a?(Hash) ? config["app"]["backend_url"] : nil) + config["ruflet_client_url"], + (config["app"].is_a?(Hash) ? config["app"]["ruflet_client_url"] : nil) ] raw = candidates.find { |v| !v.to_s.strip.empty? } return nil if raw.nil? diff --git a/packages/ruflet_cli/lib/ruflet/cli/new_command.rb b/packages/ruflet_cli/lib/ruflet/cli/new_command.rb index dbdf3a4a..245cdb18 100644 --- a/packages/ruflet_cli/lib/ruflet/cli/new_command.rb +++ b/packages/ruflet_cli/lib/ruflet/cli/new_command.rb @@ -7,22 +7,22 @@ module Ruflet module CLI module NewCommand CLIENT_EXTENSION_MAP = { - "ads" => { package: "flet_ads", alias: "flet_ads" }, - "audio" => { package: "flet_audio", alias: "flet_audio" }, - "audio_recorder" => { package: "flet_audio_recorder", alias: "flet_audio_recorder" }, - "camera" => { package: "flet_camera", alias: "flet_camera" }, - "charts" => { package: "flet_charts", alias: "flet_charts" }, - "code_editor" => { package: "flet_code_editor", alias: "flet_code_editor" }, - "color_pickers" => { package: "flet_color_pickers", alias: "flet_color_picker" }, - "datatable2" => { package: "flet_datatable2", alias: "flet_datatable2" }, - "flashlight" => { package: "flet_flashlight", alias: "flet_flashlight" }, - "geolocator" => { package: "flet_geolocator", alias: "flet_geolocator" }, - "lottie" => { package: "flet_lottie", alias: "flet_lottie" }, - "map" => { package: "flet_map", alias: "flet_map" }, - "permission_handler" => { package: "flet_permission_handler", alias: "flet_permission_handler" }, - "secure_storage" => { package: "flet_secure_storage", alias: "flet_secure_storage" }, - "video" => { package: "flet_video", alias: "flet_video" }, - "webview" => { package: "flet_webview", alias: "flet_webview" } + "ads" => { package: "flet_ads", alias: "ruflet_ads" }, + "audio" => { package: "flet_audio", alias: "ruflet_audio" }, + "audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" }, + "camera" => { package: "flet_camera", alias: "ruflet_camera" }, + "charts" => { package: "flet_charts", alias: "ruflet_charts" }, + "code_editor" => { package: "flet_code_editor", alias: "ruflet_code_editor" }, + "color_pickers" => { package: "flet_color_pickers", alias: "ruflet_color_picker" }, + "datatable2" => { package: "flet_datatable2", alias: "ruflet_datatable2" }, + "flashlight" => { package: "flet_flashlight", alias: "ruflet_flashlight" }, + "geolocator" => { package: "flet_geolocator", alias: "ruflet_geolocator" }, + "lottie" => { package: "flet_lottie", alias: "ruflet_lottie" }, + "map" => { package: "flet_map", alias: "ruflet_map" }, + "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" }, + "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" }, + "video" => { package: "flet_video", alias: "ruflet_video" }, + "webview" => { package: "flet_webview", alias: "ruflet_webview" } }.freeze def command_new(args) @@ -95,9 +95,9 @@ def write_default_ruflet_config(root, app_name) File.write(File.join(root, "ruflet.yaml"), <<~YAML) app: name: #{app_name} - # Optional production backend endpoint used by `ruflet build`. + # Optional production client endpoint used by `ruflet build`. # Example: https://api.example.com - backend_url: "" + ruflet_client_url: "" # Source of truth for Flutter client extensions/plugins. # Examples: camera, video, audio, flashlight, webview, map diff --git a/packages/ruflet_cli/test/new_command_test.rb b/packages/ruflet_cli/test/new_command_test.rb index 9a1b6276..1f88b184 100644 --- a/packages/ruflet_cli/test/new_command_test.rb +++ b/packages/ruflet_cli/test/new_command_test.rb @@ -65,25 +65,25 @@ def test_prune_client_manifest_keeps_only_selected_extensions File.join(client_dir, "lib", "main.dart"), <<~DART import 'package:flet/flet.dart'; - import 'package:flet_camera/flet_camera.dart' as flet_camera; - import 'package:flet_video/flet_video.dart' as flet_video; + import 'package:flet_camera/flet_camera.dart' as ruflet_camera; + import 'package:flet_video/flet_video.dart' as ruflet_video; final extensions = [ - flet_camera.Extension(), - flet_video.Extension(), + ruflet_camera.Extension(), + ruflet_video.Extension(), ]; DART ) - Ruflet::CLI.send(:apply_client_manifest!, client_dir, ["flet_camera"], ["flet_camera"]) + Ruflet::CLI.send(:apply_client_manifest!, client_dir, ["flet_camera"], ["ruflet_camera"]) pruned_pubspec = File.read(File.join(client_dir, "pubspec.yaml")) pruned_main = File.read(File.join(client_dir, "lib", "main.dart")) assert_includes pruned_pubspec, "flet_camera:" refute_includes pruned_pubspec, "flet_video:" - assert_includes pruned_main, "flet_camera.Extension()" - refute_includes pruned_main, "flet_video.Extension()" + assert_includes pruned_main, "ruflet_camera.Extension()" + refute_includes pruned_main, "ruflet_video.Extension()" end end end diff --git a/packages/ruflet_server/lib/ruflet/server.rb b/packages/ruflet_server/lib/ruflet/server.rb index 46fa0962..cb489069 100644 --- a/packages/ruflet_server/lib/ruflet/server.rb +++ b/packages/ruflet_server/lib/ruflet/server.rb @@ -196,6 +196,8 @@ def handle_socket(socket) handle_http_request(socket, path) end rescue StandardError => e + return if disconnect_error?(e) + warn "server error: #{e.class}: #{e.message}" warn e.backtrace.join("\n") if e.backtrace send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") }) if ws @@ -211,6 +213,8 @@ def run_connection(ws) handle_message(ws, raw) end rescue StandardError => e + return if disconnect_error?(e) + warn "server error: #{e.class}: #{e.message}" warn e.backtrace.join("\n") if e.backtrace send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") }) @@ -518,12 +522,27 @@ def send_message(ws, action, payload) message = [action, payload] ws.send_binary(Ruflet::WireCodec.pack(message)) rescue StandardError => e - warn "send error: #{e.class}: #{e.message}" + unless disconnect_error?(e) + warn "send error: #{e.class}: #{e.message}" + end remove_session(ws) unregister_connection(ws) ws&.close end + def disconnect_error?(error) + return true if error.is_a?(IOError) + return true if error.is_a?(Errno::EPIPE) + return true if error.is_a?(Errno::ECONNRESET) + return true if error.is_a?(Errno::ECONNABORTED) + return true if error.is_a?(Errno::ENOTCONN) + return true if error.is_a?(Errno::ESHUTDOWN) + return true if error.is_a?(Errno::EBADF) + return true if error.is_a?(Errno::EINVAL) + + false + end + def pseudo_uuid now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) rnd = rand(0..0xffff_ffff) diff --git a/packages/ruflet_server/lib/ruflet/server/web_socket_connection.rb b/packages/ruflet_server/lib/ruflet/server/web_socket_connection.rb index e64fdcf6..453ca62c 100644 --- a/packages/ruflet_server/lib/ruflet/server/web_socket_connection.rb +++ b/packages/ruflet_server/lib/ruflet/server/web_socket_connection.rb @@ -123,6 +123,8 @@ def read_exact(length) end chunk + rescue IOError, SystemCallError + nil end end end diff --git a/templates/ruflet_flutter_template/lib/main.dart b/templates/ruflet_flutter_template/lib/main.dart index bae9e2a3..115d5186 100644 --- a/templates/ruflet_flutter_template/lib/main.dart +++ b/templates/ruflet_flutter_template/lib/main.dart @@ -1,32 +1,32 @@ import 'dart:async'; import 'package:flet/flet.dart'; -import 'package:flet_ads/flet_ads.dart' as flet_ads; +import 'package:flet_ads/flet_ads.dart' as ruflet_ads; // --FAT_CLIENT_START-- -import 'package:flet_audio/flet_audio.dart' as flet_audio; +import 'package:flet_audio/flet_audio.dart' as ruflet_audio; // --FAT_CLIENT_END-- import 'package:flet_audio_recorder/flet_audio_recorder.dart' - as flet_audio_recorder; -import 'package:flet_camera/flet_camera.dart' as flet_camera; -import 'package:flet_charts/flet_charts.dart' as flet_charts; -import 'package:flet_code_editor/flet_code_editor.dart' as flet_code_editor; + as ruflet_audio_recorder; +import 'package:flet_camera/flet_camera.dart' as ruflet_camera; +import 'package:flet_charts/flet_charts.dart' as ruflet_charts; +import 'package:flet_code_editor/flet_code_editor.dart' as ruflet_code_editor; import 'package:flet_color_pickers/flet_color_pickers.dart' - as flet_color_picker; -import 'package:flet_datatable2/flet_datatable2.dart' as flet_datatable2; -import 'package:flet_flashlight/flet_flashlight.dart' as flet_flashlight; -import 'package:flet_geolocator/flet_geolocator.dart' as flet_geolocator; -import 'package:flet_lottie/flet_lottie.dart' as flet_lottie; -import 'package:flet_map/flet_map.dart' as flet_map; + as ruflet_color_picker; +import 'package:flet_datatable2/flet_datatable2.dart' as ruflet_datatable2; +import 'package:flet_flashlight/flet_flashlight.dart' as ruflet_flashlight; +import 'package:flet_geolocator/flet_geolocator.dart' as ruflet_geolocator; +import 'package:flet_lottie/flet_lottie.dart' as ruflet_lottie; +import 'package:flet_map/flet_map.dart' as ruflet_map; import 'package:flet_permission_handler/flet_permission_handler.dart' - as flet_permission_handler; + as ruflet_permission_handler; // --FAT_CLIENT_START-- // --FAT_CLIENT_END-- import 'package:flet_secure_storage/flet_secure_storage.dart' - as flet_secure_storage; + as ruflet_secure_storage; // --FAT_CLIENT_START-- -import 'package:flet_video/flet_video.dart' as flet_video; +import 'package:flet_video/flet_video.dart' as ruflet_video; // --FAT_CLIENT_END-- -import 'package:flet_webview/flet_webview.dart' as flet_webview; +import 'package:flet_webview/flet_webview.dart' as ruflet_webview; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; @@ -35,8 +35,8 @@ import 'connection_probe.dart'; const bool isProduction = bool.fromEnvironment('dart.vm.product'); const int kRufletPort = 8550; -const String kConfiguredBackendUrl = - String.fromEnvironment('RUFLET_BACKEND_URL', defaultValue: ''); +const String kConfiguredClientUrl = + String.fromEnvironment('RUFLET_CLIENT_URL', defaultValue: ''); Tester? tester; String normalizePageUrlForPlatform(String rawUrl) { @@ -77,7 +77,7 @@ String fallbackBackendUrl() => normalizePageUrlForPlatform('http://0.0.0.0:$kRufletPort'); String resolveBackendUrl() { - final configured = parseBackendUrl(kConfiguredBackendUrl); + final configured = parseBackendUrl(kConfiguredClientUrl); if (configured != null) return configured; return fallbackBackendUrl(); } @@ -99,24 +99,24 @@ Future main() async { } final extensions = [ - flet_ads.Extension(), - flet_audio_recorder.Extension(), - flet_camera.Extension(), - flet_charts.Extension(), - flet_code_editor.Extension(), - flet_color_picker.Extension(), - flet_datatable2.Extension(), - flet_flashlight.Extension(), - flet_geolocator.Extension(), - flet_lottie.Extension(), - flet_map.Extension(), - flet_permission_handler.Extension(), - flet_secure_storage.Extension(), - flet_webview.Extension(), + ruflet_ads.Extension(), + ruflet_audio_recorder.Extension(), + ruflet_camera.Extension(), + ruflet_charts.Extension(), + ruflet_code_editor.Extension(), + ruflet_color_picker.Extension(), + ruflet_datatable2.Extension(), + ruflet_flashlight.Extension(), + ruflet_geolocator.Extension(), + ruflet_lottie.Extension(), + ruflet_map.Extension(), + ruflet_permission_handler.Extension(), + ruflet_secure_storage.Extension(), + ruflet_webview.Extension(), // --FAT_CLIENT_START-- - flet_audio.Extension(), - flet_video.Extension(), + ruflet_audio.Extension(), + ruflet_video.Extension(), // --FAT_CLIENT_END-- ]; From 82624f0b12d68d4b59a1fd9684cc3936e4df9882 Mon Sep 17 00:00:00 2001 From: AdamMusa Date: Mon, 9 Mar 2026 14:51:13 -0400 Subject: [PATCH 4/4] added webview in ruflet studio --- examples/ruflet_studio/sections_media/webview.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/ruflet_studio/sections_media/webview.rb b/examples/ruflet_studio/sections_media/webview.rb index 4eec6a36..af5f3043 100644 --- a/examples/ruflet_studio/sections_media/webview.rb +++ b/examples/ruflet_studio/sections_media/webview.rb @@ -4,11 +4,10 @@ module RufletStudio module SectionsMedia def build_webview(_page, _status) webview_control = web_view( - url: "https://flet.dev", + url: "https://rubyonrails.org/", method: "get", expand: true ) - container( expand: true, content: webview_control