From a5974617aa6d0bc035225003e2472015bb000e84 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 22 May 2026 18:16:31 +0200 Subject: [PATCH] Add proxy routing and sing-box support --- .gitignore | 2 + apps/weblibre/android/app/build.gradle | 16 +- .../drift_schemas/user/drift_schema_v6.json | 622 ++ .../drift_schemas/user/drift_schema_v7.json | 632 ++ .../drift_schemas/user/drift_schema_v8.json | 563 ++ apps/weblibre/lib/core/routing/routes.dart | 7 + apps/weblibre/lib/core/routing/routes.g.dart | 172 + .../lib/core/routing/routes.settings.dart | 84 + .../services/handoff_redeem_client.dart | 3 +- .../screens/account_settings.dart | 8 +- .../widgets/sync_document_list_section.dart | 89 +- .../presentation/widgets/sync_setup_card.dart | 8 +- .../geckoview/domain/providers/tab_state.dart | 21 +- .../domain/providers/tab_state.g.dart | 2 +- .../geckoview/domain/repositories/tab.dart | 8 +- .../geckoview/domain/repositories/tab.g.dart | 2 +- .../services/proxy_settings_replication.dart | 271 +- .../proxy_settings_replication.g.dart | 2 +- .../presentation/toolbar_button_registry.dart | 11 +- .../browser/presentation/screens/browser.dart | 52 + .../widgets/browser_menu_sheet.dart | 268 +- .../widgets/browser_modules/browser_view.dart | 14 +- .../presentation/widgets/sheets/view_tab.dart | 3 + .../widgets/tab_view/tab_view_header.dart | 25 +- .../domain/providers/combined_history.dart | 6 +- .../search/presentation/screens/search.dart | 26 +- .../combined_history_suggestions.dart | 4 +- .../search_modules/history_suggestions.dart | 6 +- .../local_history_suggestions.dart | 13 +- .../tabs/data/models/container_data.dart | 24 +- .../tabs/data/models/container_data.g.dart | 21 +- .../features/tabs/domain/providers.dart | 2 +- .../features/tabs/domain/providers.g.dart | 6 +- .../domain/providers/selected_container.dart | 6 +- .../providers/selected_container.g.dart | 2 +- .../tabs/domain/repositories/container.dart | 49 +- .../tabs/domain/repositories/container.g.dart | 2 +- .../domain/repositories/history_search.dart | 4 +- .../services/local_index_settings_sync.dart | 18 +- .../presentation/screens/container_edit.dart | 185 +- .../presentation/screens/container_list.dart | 35 +- .../screens/container_selection.dart | 16 +- .../proxy/data/forms/singbox_form_field.dart | 81 + .../proxy/data/forms/singbox_form_spec.dart | 168 + .../proxy/data/forms/singbox_form_specs.dart | 415 ++ .../proxy/data/models/proxy_log_message.dart | 78 + .../proxy/data/models/proxy_profile_seed.dart | 44 + .../proxy/data/models/proxy_share.dart | 141 + .../data/models/singbox_proxy_profile.dart | 39 + .../data/models/wireguard_config_import.dart | 153 + .../proxy/data/parsers/base64_text.dart | 30 + .../proxy/data/parsers/host_port.dart | 56 + .../proxy/data/parsers/singbox_proxy_uri.dart | 320 + .../features/proxy/data/proxy_connection.dart | 74 + .../singbox_proxy_profile_type_x.dart | 60 + .../providers/assigned_proxy_profiles.dart | 60 + .../providers/assigned_proxy_profiles.g.dart | 66 + .../providers/proxy_connection_options.dart | 85 + .../providers/proxy_connection_options.g.dart | 58 + .../domain/repositories/container_proxy.dart | 148 + .../repositories/container_proxy.g.dart | 63 + .../singbox_proxy_credentials.dart | 53 + .../singbox_proxy_credentials.g.dart | 66 + .../repositories/singbox_proxy_logs.dart | 78 + .../repositories/singbox_proxy_logs.g.dart | 67 + .../repositories/singbox_proxy_profiles.dart | 112 + .../singbox_proxy_profiles.g.dart | 62 + .../repositories/singbox_proxy_runtime.dart | 356 + .../repositories/singbox_proxy_runtime.g.dart | 117 + .../services/browser_dns_leak_guard.dart | 109 + .../services/browser_dns_leak_guard.g.dart | 128 + .../domain/services/dns_config_resolver.dart | 135 + .../domain/services/proxy_input_consumer.dart | 237 + .../services/proxy_input_consumer.g.dart} | 24 +- .../domain/services/proxy_latency_tester.dart | 249 + .../services/proxy_latency_tester.g.dart | 94 + .../services/singbox_proxy_endpoint_sync.dart | 117 + .../singbox_proxy_endpoint_sync.g.dart | 116 + .../services/subscription_importer.dart | 128 + .../controllers/ensure_proxy_started.dart | 180 + .../proxy_profile_draft_controller.dart | 391 ++ .../proxy_profile_draft_controller.g.dart | 296 + .../screens/proxy_routing_settings.dart | 214 + .../screens/singbox_proxy_logs.dart | 231 + .../screens/singbox_proxy_profile_editor.dart | 265 + .../screens/singbox_proxy_profiles.dart | 303 + .../screens/subscription_import.dart | 286 + .../widgets/add_proxy_method_sheet.dart | 340 + .../custom_outbound_profile_form.dart | 108 + .../profile_dns_override_section.dart | 118 + .../profile_editor_section.dart | 55 + .../structured_profile_form.dart | 297 + .../widgets/profile_list/ip_chip.dart | 49 + .../widgets/profile_list/latency_chip.dart | 136 + .../widgets/profile_list/menu_row.dart | 35 + .../profile_list/profile_subtitle.dart | 62 + .../widgets/profile_list/profile_tile.dart | 249 + .../widgets/profile_list/protocol_badge.dart | 56 + .../widgets/profile_list/run_switch.dart | 55 + .../profile_list/share_profile_dialog.dart | 114 + .../widgets/profile_list/status_header.dart | 104 + .../widgets/profile_list/tor_tile.dart | 154 + .../data/models/web_search_settings.dart | 2 +- .../domain/providers/proxy_client.dart | 7 +- .../domain/providers/proxy_client.g.dart | 2 +- .../repositories/web_search_settings.dart | 2 +- .../screens/experimental_settings.dart | 19 +- .../presentation/screens/locale_settings.dart | 18 +- .../presentation/screens/proxy_settings.dart | 111 + .../presentation/screens/settings.dart | 43 +- .../screens/ublock_filter_lists.dart | 3 +- .../widgets/small_web_browser_overlay.dart | 18 +- .../widgets/small_web_menu_sheet.dart | 31 +- .../widgets/wander_console_sheet.dart | 19 +- .../presentation/screens/sync_settings.dart | 34 +- .../tor/domain/extensions/tor_status_x.dart | 33 + .../tor/domain/repositories/tor_proxy.dart | 95 - .../tor/domain/services/tor_proxy.dart | 16 +- .../tor/domain/services/tor_proxy.g.dart | 2 +- .../tor/presentation/screens/tor_proxy.dart | 896 +-- .../widgets/tor_notification.dart | 6 +- .../data/database/daos/proxy_profile.dart | 63 + .../database/daos/proxy_profile.drift.dart | 13 + .../features/user/data/database/database.dart | 20 +- .../user/data/database/database.drift.dart | 36 +- .../user/data/database/database.steps.dart | 546 ++ .../user/data/database/definitions.drift | 16 +- .../user/data/database/definitions.drift.dart | 758 +- .../user/data/models/proxy_dns_override.dart | 57 + .../data/models/proxy_dns_override.g.dart | 33 + .../data/models/proxy_routing_settings.dart | 75 + .../data/models/proxy_routing_settings.g.dart | 136 + .../user/data/models/tor_settings.dart | 15 +- .../user/data/models/tor_settings.g.dart | 42 - .../presentation/screens/profile_backup.dart | 9 +- .../presentation/screens/profile_restore.dart | 9 +- .../repositories/proxy_routing_settings.dart | 101 + .../proxy_routing_settings.g.dart | 115 + .../domain/repositories/tor_settings.dart | 8 - .../domain/repositories/tor_settings.g.dart | 2 +- .../sandbox_capture_controller.dart | 2 +- .../domain/controllers/search_controller.dart | 8 +- .../controllers/search_controller.g.dart | 2 +- .../services/capture_artifact_downloader.dart | 2 +- .../dialogs/fetch_method_dialog.dart | 9 +- .../widgets/page_metadata_chips.dart | 2 +- .../widgets/route_through_tor_toggle.dart | 9 +- .../widgets/search_filter_chips.dart | 2 +- .../widgets/search_mode_selector.dart | 2 +- .../widgets/search_result_card.dart | 2 +- .../widgets/search_result_metadata_chips.dart | 2 +- .../widgets/web_search_infobox_card.dart | 2 +- apps/weblibre/lib/main.dart | 20 +- .../controllers/website_title.dart | 19 +- .../controllers/website_title.g.dart | 2 +- .../presentation/widgets/button_spinner.dart | 34 + .../widgets/obscurable_text_field.dart | 76 + apps/weblibre/lib/utils/ui_helper.dart | 26 +- apps/weblibre/pubspec.yaml | 6 +- .../test/drift/user/generated/schema.dart | 11 +- .../test/drift/user/generated/schema_v6.dart | 2136 ++++++ .../test/drift/user/generated/schema_v7.dart | 2183 ++++++ .../test/drift/user/generated/schema_v8.dart | 1881 +++++ .../proxy_settings_replication_test.dart | 174 + .../tabs/data/models/container_data_test.dart | 33 + .../screens/container_edit_test.dart | 86 + .../data/models/singbox_proxy_form_test.dart | 345 + .../models/wireguard_config_import_test.dart | 56 + .../proxy_connection_options_test.dart | 66 + .../singbox_proxy_profiles_test.dart | 155 + .../singbox_proxy_runtime_test.dart | 451 ++ .../services/dns_config_resolver_test.dart | 54 + .../services/singbox_tag_format_test.dart | 25 + .../ensure_proxy_started_test.dart | 130 + .../proxy_profile_draft_controller_test.dart | 207 + .../screens/singbox_proxy_profiles_test.dart | 216 + .../controllers/search_controller_test.dart | 2 +- .../domain/services/capture_server_test.dart | 72 +- .../screens/page_preview_test.dart | 6 +- .../widgets/search_result_card_test.dart | 26 +- native/go_mobile_runtime/README.md | 38 + native/go_mobile_runtime/pins.env | 12 + .../scripts/build-android.sh | 275 + .../scripts/fdroid-prebuild.sh | 7 + .../api/GeckoContainerProxyApiImpl.kt | 49 +- .../interceptor/AppRequestInterceptor.kt | 17 + .../pigeons/Gecko.g.kt | 302 +- .../src/background/BackgroundMain.ts | 2 +- .../container_proxy/src/background/index.ts | 39 +- .../container_proxy/src/store/Store.ts | 4 + .../test/unit/BackgroundMain.test.ts | 12 +- .../container_proxy/test/unit/Store.test.ts | 9 + .../lib/flutter_mozilla_components.dart | 2 + .../services/gecko_container_proxy.dart | 20 + .../lib/src/domain/services/gecko_event.dart | 9 + .../src/domain/services/gecko_history.dart | 9 +- .../lib/src/pigeons/gecko.g.dart | 6119 ++++++++++------- .../pigeons/gecko.dart | 45 + packages/flutter_singbox_proxy/.gitignore | 37 + packages/flutter_singbox_proxy/.metadata | 30 + packages/flutter_singbox_proxy/CHANGELOG.md | 3 + packages/flutter_singbox_proxy/LICENSE | 1 + packages/flutter_singbox_proxy/README.md | 59 + .../analysis_options.yaml | 4 + .../flutter_singbox_proxy/android/.gitignore | 9 + .../android/build.gradle | 81 + .../android/libs/.gitkeep | 0 .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../FlutterSingboxProxyPlugin.kt | 33 + .../flutter_singbox_proxy/LibboxRuntime.kt | 485 ++ .../PlatformDohResolver.kt | 75 + .../SingboxConfigBuilder.kt | 398 ++ .../SingboxRuntimeManager.kt | 286 + .../flutter_singbox_proxy/SingboxTagFormat.kt | 17 + .../generated/SingboxProxyApi.g.kt | 947 +++ .../FlutterSingboxProxyPluginTest.kt | 247 + .../SingboxRuntimeManagerTest.kt | 84 + .../SingboxTagFormatTest.kt | 39 + .../flutter_singbox_proxy/example/.gitignore | 45 + .../flutter_singbox_proxy/example/README.md | 17 + .../example/analysis_options.yaml | 4 + .../example/android/.gitignore | 14 + .../example/android/app/build.gradle | 38 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 + .../MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../example/android/build.gradle | 18 + .../example/android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../example/android/settings.gradle | 25 + .../plugin_integration_test.dart | 23 + .../example/lib/main.dart | 27 + .../example/pubspec.yaml | 86 + .../example/test/widget_test.dart | 28 + .../lib/flutter_singbox_proxy.dart | 71 + .../lib/src/singbox_proxy_api.g.dart | 912 +++ .../pigeons/singbox_proxy_api.dart | 227 + packages/flutter_singbox_proxy/pubspec.yaml | 71 + .../scripts/build-libbox-android.sh | 130 + .../scripts/fdroid-prebuild.sh | 8 + .../test/flutter_singbox_proxy_test.dart | 25 + packages/flutter_tor/android/build.gradle | 15 +- .../weblibre/flutter_tor/FlutterTorPlugin.kt | 1 - .../flutter_tor/PluggableTransportManager.kt | 6 +- .../eu/weblibre/flutter_tor/ProxyImpl.kt | 7 +- .../eu/weblibre/flutter_tor/TorConfig.kt | 2 +- packages/flutter_tor/lib/src/tor_api.g.dart | 201 +- .../lib/src/pigeons/locales.g.dart | 62 +- .../lib/src/pigeons/intent.g.dart | 147 +- .../lib/src/pigeons/speech_to_text.g.dart | 62 +- pubspec.yaml | 5 + 262 files changed, 32003 insertions(+), 3962 deletions(-) create mode 100644 apps/weblibre/drift_schemas/user/drift_schema_v6.json create mode 100644 apps/weblibre/drift_schemas/user/drift_schema_v7.json create mode 100644 apps/weblibre/drift_schemas/user/drift_schema_v8.json create mode 100644 apps/weblibre/lib/features/proxy/data/forms/singbox_form_field.dart create mode 100644 apps/weblibre/lib/features/proxy/data/forms/singbox_form_spec.dart create mode 100644 apps/weblibre/lib/features/proxy/data/forms/singbox_form_specs.dart create mode 100644 apps/weblibre/lib/features/proxy/data/models/proxy_log_message.dart create mode 100644 apps/weblibre/lib/features/proxy/data/models/proxy_profile_seed.dart create mode 100644 apps/weblibre/lib/features/proxy/data/models/proxy_share.dart create mode 100644 apps/weblibre/lib/features/proxy/data/models/singbox_proxy_profile.dart create mode 100644 apps/weblibre/lib/features/proxy/data/models/wireguard_config_import.dart create mode 100644 apps/weblibre/lib/features/proxy/data/parsers/base64_text.dart create mode 100644 apps/weblibre/lib/features/proxy/data/parsers/host_port.dart create mode 100644 apps/weblibre/lib/features/proxy/data/parsers/singbox_proxy_uri.dart create mode 100644 apps/weblibre/lib/features/proxy/data/proxy_connection.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/extensions/singbox_proxy_profile_type_x.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/providers/assigned_proxy_profiles.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/providers/assigned_proxy_profiles.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/providers/proxy_connection_options.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/providers/proxy_connection_options.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/container_proxy.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/container_proxy.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_credentials.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_credentials.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_profiles.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_profiles.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_runtime.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_runtime.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/services/browser_dns_leak_guard.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/services/browser_dns_leak_guard.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/services/dns_config_resolver.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/services/proxy_input_consumer.dart rename apps/weblibre/lib/features/{tor/domain/repositories/tor_proxy.g.dart => proxy/domain/services/proxy_input_consumer.g.dart} (65%) create mode 100644 apps/weblibre/lib/features/proxy/domain/services/proxy_latency_tester.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/services/proxy_latency_tester.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/services/singbox_proxy_endpoint_sync.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/services/singbox_proxy_endpoint_sync.g.dart create mode 100644 apps/weblibre/lib/features/proxy/domain/services/subscription_importer.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/controllers/ensure_proxy_started.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/controllers/proxy_profile_draft_controller.g.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/screens/proxy_routing_settings.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_logs.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_profile_editor.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_profiles.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/screens/subscription_import.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/add_proxy_method_sheet.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/custom_outbound_profile_form.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/profile_dns_override_section.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/profile_editor_section.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/ip_chip.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/latency_chip.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/menu_row.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/profile_subtitle.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/profile_tile.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/protocol_badge.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/run_switch.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/share_profile_dialog.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/status_header.dart create mode 100644 apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/tor_tile.dart create mode 100644 apps/weblibre/lib/features/settings/presentation/screens/proxy_settings.dart create mode 100644 apps/weblibre/lib/features/tor/domain/extensions/tor_status_x.dart delete mode 100644 apps/weblibre/lib/features/tor/domain/repositories/tor_proxy.dart create mode 100644 apps/weblibre/lib/features/user/data/database/daos/proxy_profile.dart create mode 100644 apps/weblibre/lib/features/user/data/database/daos/proxy_profile.drift.dart create mode 100644 apps/weblibre/lib/features/user/data/models/proxy_dns_override.dart create mode 100644 apps/weblibre/lib/features/user/data/models/proxy_dns_override.g.dart create mode 100644 apps/weblibre/lib/features/user/data/models/proxy_routing_settings.dart create mode 100644 apps/weblibre/lib/features/user/data/models/proxy_routing_settings.g.dart create mode 100644 apps/weblibre/lib/features/user/domain/repositories/proxy_routing_settings.dart create mode 100644 apps/weblibre/lib/features/user/domain/repositories/proxy_routing_settings.g.dart create mode 100644 apps/weblibre/lib/presentation/widgets/button_spinner.dart create mode 100644 apps/weblibre/lib/presentation/widgets/obscurable_text_field.dart create mode 100644 apps/weblibre/test/drift/user/generated/schema_v6.dart create mode 100644 apps/weblibre/test/drift/user/generated/schema_v7.dart create mode 100644 apps/weblibre/test/drift/user/generated/schema_v8.dart create mode 100644 apps/weblibre/test/features/geckoview/features/browser/domain/services/proxy_settings_replication_test.dart create mode 100644 apps/weblibre/test/features/geckoview/features/tabs/data/models/container_data_test.dart create mode 100644 apps/weblibre/test/features/geckoview/features/tabs/presentation/screens/container_edit_test.dart create mode 100644 apps/weblibre/test/features/proxy/data/models/singbox_proxy_form_test.dart create mode 100644 apps/weblibre/test/features/proxy/data/models/wireguard_config_import_test.dart create mode 100644 apps/weblibre/test/features/proxy/domain/providers/proxy_connection_options_test.dart create mode 100644 apps/weblibre/test/features/proxy/domain/repositories/singbox_proxy_profiles_test.dart create mode 100644 apps/weblibre/test/features/proxy/domain/repositories/singbox_proxy_runtime_test.dart create mode 100644 apps/weblibre/test/features/proxy/domain/services/dns_config_resolver_test.dart create mode 100644 apps/weblibre/test/features/proxy/domain/services/singbox_tag_format_test.dart create mode 100644 apps/weblibre/test/features/proxy/presentation/controllers/ensure_proxy_started_test.dart create mode 100644 apps/weblibre/test/features/proxy/presentation/controllers/proxy_profile_draft_controller_test.dart create mode 100644 apps/weblibre/test/features/proxy/presentation/screens/singbox_proxy_profiles_test.dart create mode 100644 native/go_mobile_runtime/README.md create mode 100644 native/go_mobile_runtime/pins.env create mode 100755 native/go_mobile_runtime/scripts/build-android.sh create mode 100755 native/go_mobile_runtime/scripts/fdroid-prebuild.sh create mode 100644 packages/flutter_singbox_proxy/.gitignore create mode 100644 packages/flutter_singbox_proxy/.metadata create mode 100644 packages/flutter_singbox_proxy/CHANGELOG.md create mode 100644 packages/flutter_singbox_proxy/LICENSE create mode 100644 packages/flutter_singbox_proxy/README.md create mode 100644 packages/flutter_singbox_proxy/analysis_options.yaml create mode 100644 packages/flutter_singbox_proxy/android/.gitignore create mode 100644 packages/flutter_singbox_proxy/android/build.gradle create mode 100644 packages/flutter_singbox_proxy/android/libs/.gitkeep create mode 100644 packages/flutter_singbox_proxy/android/settings.gradle create mode 100644 packages/flutter_singbox_proxy/android/src/main/AndroidManifest.xml create mode 100644 packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/FlutterSingboxProxyPlugin.kt create mode 100644 packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/LibboxRuntime.kt create mode 100644 packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/PlatformDohResolver.kt create mode 100644 packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxConfigBuilder.kt create mode 100644 packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxRuntimeManager.kt create mode 100644 packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxTagFormat.kt create mode 100644 packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/generated/SingboxProxyApi.g.kt create mode 100644 packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/FlutterSingboxProxyPluginTest.kt create mode 100644 packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxRuntimeManagerTest.kt create mode 100644 packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxTagFormatTest.kt create mode 100644 packages/flutter_singbox_proxy/example/.gitignore create mode 100644 packages/flutter_singbox_proxy/example/README.md create mode 100644 packages/flutter_singbox_proxy/example/analysis_options.yaml create mode 100644 packages/flutter_singbox_proxy/example/android/.gitignore create mode 100644 packages/flutter_singbox_proxy/example/android/app/build.gradle create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/kotlin/eu/weblibre/flutter_singbox_proxy_example/MainActivity.kt create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/flutter_singbox_proxy/example/android/app/src/profile/AndroidManifest.xml create mode 100644 packages/flutter_singbox_proxy/example/android/build.gradle create mode 100644 packages/flutter_singbox_proxy/example/android/gradle.properties create mode 100644 packages/flutter_singbox_proxy/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/flutter_singbox_proxy/example/android/settings.gradle create mode 100644 packages/flutter_singbox_proxy/example/integration_test/plugin_integration_test.dart create mode 100644 packages/flutter_singbox_proxy/example/lib/main.dart create mode 100644 packages/flutter_singbox_proxy/example/pubspec.yaml create mode 100644 packages/flutter_singbox_proxy/example/test/widget_test.dart create mode 100644 packages/flutter_singbox_proxy/lib/flutter_singbox_proxy.dart create mode 100644 packages/flutter_singbox_proxy/lib/src/singbox_proxy_api.g.dart create mode 100644 packages/flutter_singbox_proxy/pigeons/singbox_proxy_api.dart create mode 100644 packages/flutter_singbox_proxy/pubspec.yaml create mode 100755 packages/flutter_singbox_proxy/scripts/build-libbox-android.sh create mode 100755 packages/flutter_singbox_proxy/scripts/fdroid-prebuild.sh create mode 100644 packages/flutter_singbox_proxy/test/flutter_singbox_proxy_test.dart diff --git a/.gitignore b/.gitignore index 892ec1b7..8d442f3f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ plans/ .temp/ certs/ codenomad/ +native/go_mobile_runtime/.work/ +native/go_mobile_runtime/build/ diff --git a/apps/weblibre/android/app/build.gradle b/apps/weblibre/android/app/build.gradle index e11f95da..331d7371 100644 --- a/apps/weblibre/android/app/build.gradle +++ b/apps/weblibre/android/app/build.gradle @@ -24,6 +24,8 @@ if (flutterVersionName == null) { flutterVersionName = "1.0" } +def weblibreGoMobileAar = file("../../../../native/go_mobile_runtime/build/weblibre-go.aar") + android { namespace = "eu.weblibre.gecko" compileSdk = 36 @@ -64,7 +66,19 @@ android { dependencies { // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.glance:glance-appwidget:1.1.1' + if (!weblibreGoMobileAar.exists()) { + throw new GradleException( + "Missing combined gomobile runtime AAR: ${weblibreGoMobileAar}. " + + "Run native/go_mobile_runtime/scripts/build-android.sh from the repository root." + ) + } + implementation files(weblibreGoMobileAar) + implementation('androidx.glance:glance-appwidget') { + version { + strictly '1.1.1' + } + because 'home_widget uses 1.+ and newer alpha releases require AGP 9.1 and compileSdk 37' + } implementation 'com.google.android.material:material:1.13.0' } diff --git a/apps/weblibre/drift_schemas/user/drift_schema_v6.json b/apps/weblibre/drift_schemas/user/drift_schema_v6.json new file mode 100644 index 00000000..465d9e21 --- /dev/null +++ b/apps/weblibre/drift_schemas/user/drift_schema_v6.json @@ -0,0 +1,622 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "setting", + "was_declared_in_moor": true, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "partition_key", + "getter_name": "partitionKey", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "any", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "strict": true + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "proxy_profile", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(SingboxProxyProfileType.values)", + "dart_type_name": "SingboxProxyProfileType" + } + }, + { + "name": "config_json", + "getter_name": "configJson", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT CURRENT_TIMESTAMP", + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT CURRENT_TIMESTAMP", + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 2, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_proxy_profile_updated_at", + "sql": "CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile(updated_at);", + "unique": false, + "columns": [] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "proxy_routing_setting", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY CHECK (id = 1)", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "regular_tabs_mode", + "getter_name": "regularTabsMode", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ProxyRegularTabRoutingMode.values)", + "dart_type_name": "ProxyRegularTabRoutingMode" + } + }, + { + "name": "regular_tabs_proxy_connection_id", + "getter_name": "regularTabsProxyConnectionId", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "private_tabs_proxy_connection_id", + "getter_name": "privateTabsProxyConnectionId", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "icon_cache", + "was_declared_in_moor": true, + "columns": [ + { + "name": "origin", + "getter_name": "origin", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "icon_data", + "getter_name": "iconData", + "moor_type": "blob", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "fetch_date", + "getter_name": "fetchDate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "onboarding", + "was_declared_in_moor": true, + "columns": [ + { + "name": "revision", + "getter_name": "revision", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "completion_date", + "getter_name": "completionDate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "riverpod", + "was_declared_in_moor": true, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "json", + "getter_name": "json", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "expireAt", + "getter_name": "expireAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "destroyKey", + "getter_name": "destroyKey", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [] + } + }, + { + "id": 7, + "references": [], + "type": "table", + "data": { + "name": "toolbar_button_configs", + "was_declared_in_moor": true, + "columns": [ + { + "name": "button_id", + "getter_name": "buttonId", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "order_key", + "getter_name": "orderKey", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT TRUE", + "default_dart": "const CustomExpression('TRUE')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "fallback_id", + "getter_name": "fallbackId", + "moor_type": "string", + "nullable": true, + "customConstraints": "REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 8, + "references": [ + 7 + ], + "type": "index", + "data": { + "on": 7, + "name": "idx_toolbar_order_key", + "sql": "CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs(order_key);", + "unique": false, + "columns": [] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "search_tokens", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": "PRIMARY KEY AUTOINCREMENT", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "token", + "getter_name": "token", + "moor_type": "blob", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "inserted_at", + "getter_name": "insertedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "issuer_key_version", + "getter_name": "issuerKeyVersion", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reserved_at", + "getter_name": "reservedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 10, + "references": [ + 9 + ], + "type": "index", + "data": { + "on": 9, + "name": "idx_search_tokens_inserted_at", + "sql": "CREATE INDEX idx_search_tokens_inserted_at ON search_tokens(inserted_at);", + "unique": false, + "columns": [] + } + }, + { + "id": 11, + "references": [ + 9 + ], + "type": "index", + "data": { + "on": 9, + "name": "idx_search_tokens_reserved_at", + "sql": "CREATE INDEX idx_search_tokens_reserved_at ON search_tokens(reserved_at);", + "unique": false, + "columns": [] + } + } + ], + "fixed_sql": [ + { + "name": "setting", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"setting\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"partition_key\" TEXT, \"value\" ANY) STRICT;" + } + ] + }, + { + "name": "proxy_profile", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"proxy_profile\" (\"id\" TEXT NOT NULL PRIMARY KEY, \"name\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"config_json\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP);" + } + ] + }, + { + "name": "idx_proxy_profile_updated_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)" + } + ] + }, + { + "name": "proxy_routing_setting", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"proxy_routing_setting\" (\"id\" INTEGER NOT NULL PRIMARY KEY CHECK (id = 1), \"regular_tabs_mode\" TEXT NOT NULL, \"regular_tabs_proxy_connection_id\" TEXT, \"private_tabs_proxy_connection_id\" TEXT);" + } + ] + }, + { + "name": "icon_cache", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"icon_cache\" (\"origin\" TEXT PRIMARY KEY NOT NULL, \"icon_data\" BLOB NOT NULL, \"fetch_date\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "onboarding", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"onboarding\" (\"revision\" INTEGER NOT NULL, \"completion_date\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "riverpod", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"riverpod\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"json\" TEXT NOT NULL, \"expireAt\" INTEGER, \"destroyKey\" TEXT) WITHOUT ROWID;" + } + ] + }, + { + "name": "toolbar_button_configs", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"toolbar_button_configs\" (\"button_id\" TEXT NOT NULL PRIMARY KEY, \"order_key\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT TRUE, \"fallback_id\" TEXT REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL);" + } + ] + }, + { + "name": "idx_toolbar_order_key", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)" + } + ] + }, + { + "name": "search_tokens", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"search_tokens\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT, \"token\" BLOB NOT NULL, \"inserted_at\" INTEGER NOT NULL, \"issuer_key_version\" TEXT NOT NULL, \"reserved_at\" INTEGER);" + } + ] + }, + { + "name": "idx_search_tokens_inserted_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)" + } + ] + }, + { + "name": "idx_search_tokens_reserved_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)" + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/weblibre/drift_schemas/user/drift_schema_v7.json b/apps/weblibre/drift_schemas/user/drift_schema_v7.json new file mode 100644 index 00000000..337653c7 --- /dev/null +++ b/apps/weblibre/drift_schemas/user/drift_schema_v7.json @@ -0,0 +1,632 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "setting", + "was_declared_in_moor": true, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "partition_key", + "getter_name": "partitionKey", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "any", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "strict": true + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "proxy_profile", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(SingboxProxyProfileType.values)", + "dart_type_name": "SingboxProxyProfileType" + } + }, + { + "name": "config_json", + "getter_name": "configJson", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "dns_override_json", + "getter_name": "dnsOverrideJson", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT CURRENT_TIMESTAMP", + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT CURRENT_TIMESTAMP", + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 2, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_proxy_profile_updated_at", + "sql": "CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile(updated_at);", + "unique": false, + "columns": [] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "proxy_routing_setting", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY CHECK (id = 1)", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "regular_tabs_mode", + "getter_name": "regularTabsMode", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ProxyRegularTabRoutingMode.values)", + "dart_type_name": "ProxyRegularTabRoutingMode" + } + }, + { + "name": "regular_tabs_proxy_connection_id", + "getter_name": "regularTabsProxyConnectionId", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "private_tabs_proxy_connection_id", + "getter_name": "privateTabsProxyConnectionId", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "icon_cache", + "was_declared_in_moor": true, + "columns": [ + { + "name": "origin", + "getter_name": "origin", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "icon_data", + "getter_name": "iconData", + "moor_type": "blob", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "fetch_date", + "getter_name": "fetchDate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "onboarding", + "was_declared_in_moor": true, + "columns": [ + { + "name": "revision", + "getter_name": "revision", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "completion_date", + "getter_name": "completionDate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "riverpod", + "was_declared_in_moor": true, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "json", + "getter_name": "json", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "expireAt", + "getter_name": "expireAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "destroyKey", + "getter_name": "destroyKey", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [] + } + }, + { + "id": 7, + "references": [], + "type": "table", + "data": { + "name": "toolbar_button_configs", + "was_declared_in_moor": true, + "columns": [ + { + "name": "button_id", + "getter_name": "buttonId", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "order_key", + "getter_name": "orderKey", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT TRUE", + "default_dart": "const CustomExpression('TRUE')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "fallback_id", + "getter_name": "fallbackId", + "moor_type": "string", + "nullable": true, + "customConstraints": "REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 8, + "references": [ + 7 + ], + "type": "index", + "data": { + "on": 7, + "name": "idx_toolbar_order_key", + "sql": "CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs(order_key);", + "unique": false, + "columns": [] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "search_tokens", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": "PRIMARY KEY AUTOINCREMENT", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "token", + "getter_name": "token", + "moor_type": "blob", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "inserted_at", + "getter_name": "insertedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "issuer_key_version", + "getter_name": "issuerKeyVersion", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reserved_at", + "getter_name": "reservedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 10, + "references": [ + 9 + ], + "type": "index", + "data": { + "on": 9, + "name": "idx_search_tokens_inserted_at", + "sql": "CREATE INDEX idx_search_tokens_inserted_at ON search_tokens(inserted_at);", + "unique": false, + "columns": [] + } + }, + { + "id": 11, + "references": [ + 9 + ], + "type": "index", + "data": { + "on": 9, + "name": "idx_search_tokens_reserved_at", + "sql": "CREATE INDEX idx_search_tokens_reserved_at ON search_tokens(reserved_at);", + "unique": false, + "columns": [] + } + } + ], + "fixed_sql": [ + { + "name": "setting", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"setting\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"partition_key\" TEXT, \"value\" ANY) STRICT;" + } + ] + }, + { + "name": "proxy_profile", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"proxy_profile\" (\"id\" TEXT NOT NULL PRIMARY KEY, \"name\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"config_json\" TEXT NOT NULL, \"dns_override_json\" TEXT, \"created_at\" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP);" + } + ] + }, + { + "name": "idx_proxy_profile_updated_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)" + } + ] + }, + { + "name": "proxy_routing_setting", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"proxy_routing_setting\" (\"id\" INTEGER NOT NULL PRIMARY KEY CHECK (id = 1), \"regular_tabs_mode\" TEXT NOT NULL, \"regular_tabs_proxy_connection_id\" TEXT, \"private_tabs_proxy_connection_id\" TEXT);" + } + ] + }, + { + "name": "icon_cache", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"icon_cache\" (\"origin\" TEXT PRIMARY KEY NOT NULL, \"icon_data\" BLOB NOT NULL, \"fetch_date\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "onboarding", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"onboarding\" (\"revision\" INTEGER NOT NULL, \"completion_date\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "riverpod", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"riverpod\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"json\" TEXT NOT NULL, \"expireAt\" INTEGER, \"destroyKey\" TEXT) WITHOUT ROWID;" + } + ] + }, + { + "name": "toolbar_button_configs", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"toolbar_button_configs\" (\"button_id\" TEXT NOT NULL PRIMARY KEY, \"order_key\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT TRUE, \"fallback_id\" TEXT REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL);" + } + ] + }, + { + "name": "idx_toolbar_order_key", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)" + } + ] + }, + { + "name": "search_tokens", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"search_tokens\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT, \"token\" BLOB NOT NULL, \"inserted_at\" INTEGER NOT NULL, \"issuer_key_version\" TEXT NOT NULL, \"reserved_at\" INTEGER);" + } + ] + }, + { + "name": "idx_search_tokens_inserted_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)" + } + ] + }, + { + "name": "idx_search_tokens_reserved_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)" + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/weblibre/drift_schemas/user/drift_schema_v8.json b/apps/weblibre/drift_schemas/user/drift_schema_v8.json new file mode 100644 index 00000000..e3a9f9b9 --- /dev/null +++ b/apps/weblibre/drift_schemas/user/drift_schema_v8.json @@ -0,0 +1,563 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "setting", + "was_declared_in_moor": true, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "partition_key", + "getter_name": "partitionKey", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "any", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "strict": true + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "proxy_profile", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(SingboxProxyProfileType.values)", + "dart_type_name": "SingboxProxyProfileType" + } + }, + { + "name": "config_json", + "getter_name": "configJson", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "dns_override_json", + "getter_name": "dnsOverrideJson", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT CURRENT_TIMESTAMP", + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT CURRENT_TIMESTAMP", + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 2, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_proxy_profile_updated_at", + "sql": "CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile(updated_at);", + "unique": false, + "columns": [] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "icon_cache", + "was_declared_in_moor": true, + "columns": [ + { + "name": "origin", + "getter_name": "origin", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "icon_data", + "getter_name": "iconData", + "moor_type": "blob", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "fetch_date", + "getter_name": "fetchDate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "onboarding", + "was_declared_in_moor": true, + "columns": [ + { + "name": "revision", + "getter_name": "revision", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "completion_date", + "getter_name": "completionDate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "riverpod", + "was_declared_in_moor": true, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "json", + "getter_name": "json", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "expireAt", + "getter_name": "expireAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "destroyKey", + "getter_name": "destroyKey", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "toolbar_button_configs", + "was_declared_in_moor": true, + "columns": [ + { + "name": "button_id", + "getter_name": "buttonId", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "order_key", + "getter_name": "orderKey", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT TRUE", + "default_dart": "const CustomExpression('TRUE')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "fallback_id", + "getter_name": "fallbackId", + "moor_type": "string", + "nullable": true, + "customConstraints": "REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 7, + "references": [ + 6 + ], + "type": "index", + "data": { + "on": 6, + "name": "idx_toolbar_order_key", + "sql": "CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs(order_key);", + "unique": false, + "columns": [] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "search_tokens", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": "PRIMARY KEY AUTOINCREMENT", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "token", + "getter_name": "token", + "moor_type": "blob", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "inserted_at", + "getter_name": "insertedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "issuer_key_version", + "getter_name": "issuerKeyVersion", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reserved_at", + "getter_name": "reservedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 9, + "references": [ + 8 + ], + "type": "index", + "data": { + "on": 8, + "name": "idx_search_tokens_inserted_at", + "sql": "CREATE INDEX idx_search_tokens_inserted_at ON search_tokens(inserted_at);", + "unique": false, + "columns": [] + } + }, + { + "id": 10, + "references": [ + 8 + ], + "type": "index", + "data": { + "on": 8, + "name": "idx_search_tokens_reserved_at", + "sql": "CREATE INDEX idx_search_tokens_reserved_at ON search_tokens(reserved_at);", + "unique": false, + "columns": [] + } + } + ], + "fixed_sql": [ + { + "name": "setting", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"setting\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"partition_key\" TEXT, \"value\" ANY) STRICT;" + } + ] + }, + { + "name": "proxy_profile", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"proxy_profile\" (\"id\" TEXT NOT NULL PRIMARY KEY, \"name\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"config_json\" TEXT NOT NULL, \"dns_override_json\" TEXT, \"created_at\" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP);" + } + ] + }, + { + "name": "idx_proxy_profile_updated_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)" + } + ] + }, + { + "name": "icon_cache", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"icon_cache\" (\"origin\" TEXT PRIMARY KEY NOT NULL, \"icon_data\" BLOB NOT NULL, \"fetch_date\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "onboarding", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"onboarding\" (\"revision\" INTEGER NOT NULL, \"completion_date\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "riverpod", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"riverpod\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"json\" TEXT NOT NULL, \"expireAt\" INTEGER, \"destroyKey\" TEXT) WITHOUT ROWID;" + } + ] + }, + { + "name": "toolbar_button_configs", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"toolbar_button_configs\" (\"button_id\" TEXT NOT NULL PRIMARY KEY, \"order_key\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT TRUE, \"fallback_id\" TEXT REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL);" + } + ] + }, + { + "name": "idx_toolbar_order_key", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)" + } + ] + }, + { + "name": "search_tokens", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"search_tokens\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT, \"token\" BLOB NOT NULL, \"inserted_at\" INTEGER NOT NULL, \"issuer_key_version\" TEXT NOT NULL, \"reserved_at\" INTEGER);" + } + ] + }, + { + "name": "idx_search_tokens_inserted_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)" + } + ] + }, + { + "name": "idx_search_tokens_reserved_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)" + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/weblibre/lib/core/routing/routes.dart b/apps/weblibre/lib/core/routing/routes.dart index 5a0d0df0..9afb1110 100644 --- a/apps/weblibre/lib/core/routing/routes.dart +++ b/apps/weblibre/lib/core/routing/routes.dart @@ -61,6 +61,12 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/c import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_list.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_selection.dart'; import 'package:weblibre/features/onboarding/presentation/onboarding.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_profile_seed.dart'; +import 'package:weblibre/features/proxy/presentation/screens/proxy_routing_settings.dart'; +import 'package:weblibre/features/proxy/presentation/screens/singbox_proxy_logs.dart'; +import 'package:weblibre/features/proxy/presentation/screens/singbox_proxy_profile_editor.dart'; +import 'package:weblibre/features/proxy/presentation/screens/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/presentation/screens/subscription_import.dart'; import 'package:weblibre/features/settings/presentation/screens/addon_collection.dart'; import 'package:weblibre/features/settings/presentation/screens/advanced_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart'; @@ -75,6 +81,7 @@ import 'package:weblibre/features/settings/presentation/screens/fingerprint_sett import 'package:weblibre/features/settings/presentation/screens/general_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/locale_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/privacy_security_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/proxy_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/search_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/settings.dart'; import 'package:weblibre/features/settings/presentation/screens/toolbar_layout_settings.dart'; diff --git a/apps/weblibre/lib/core/routing/routes.g.dart b/apps/weblibre/lib/core/routing/routes.g.dart index 32079146..64315fa8 100644 --- a/apps/weblibre/lib/core/routing/routes.g.dart +++ b/apps/weblibre/lib/core/routing/routes.g.dart @@ -1660,6 +1660,38 @@ RouteBase get $settingsRoute => GoRouteData.$route( name: 'ContextualToolbarSettingsRoute', factory: $ContextualToolbarSettingsRoute._fromState, ), + GoRouteData.$route( + path: 'singbox_proxy_profiles', + name: 'SingboxProxyProfilesRoute', + factory: $SingboxProxyProfilesRoute._fromState, + routes: [ + GoRouteData.$route( + path: 'editor', + name: 'SingboxProxyProfileEditorRoute', + factory: $SingboxProxyProfileEditorRoute._fromState, + ), + GoRouteData.$route( + path: 'logs', + name: 'SingboxProxyLogsRoute', + factory: $SingboxProxyLogsRoute._fromState, + ), + GoRouteData.$route( + path: 'subscription', + name: 'SubscriptionImportRoute', + factory: $SubscriptionImportRoute._fromState, + ), + ], + ), + GoRouteData.$route( + path: 'proxy_routing', + name: 'ProxyRoutingSettingsRoute', + factory: $ProxyRoutingSettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'proxy', + name: 'ProxySettingsRoute', + factory: $ProxySettingsRoute._fromState, + ), ], ); @@ -2213,6 +2245,146 @@ mixin $ContextualToolbarSettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin $SingboxProxyProfilesRoute on GoRouteData { + static SingboxProxyProfilesRoute _fromState(GoRouterState state) => + const SingboxProxyProfilesRoute(); + + @override + String get location => + GoRouteData.$location('/settings/singbox_proxy_profiles'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $SingboxProxyProfileEditorRoute on GoRouteData { + static SingboxProxyProfileEditorRoute _fromState(GoRouterState state) => + SingboxProxyProfileEditorRoute( + profileId: state.uri.queryParameters['profile-id'], + $extra: state.extra as ProxyProfileSeed?, + ); + + SingboxProxyProfileEditorRoute get _self => + this as SingboxProxyProfileEditorRoute; + + @override + String get location => GoRouteData.$location( + '/settings/singbox_proxy_profiles/editor', + queryParams: {if (_self.profileId != null) 'profile-id': _self.profileId}, + ); + + @override + void go(BuildContext context) => context.go(location, extra: _self.$extra); + + @override + Future push(BuildContext context) => + context.push(location, extra: _self.$extra); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: _self.$extra); + + @override + void replace(BuildContext context) => + context.replace(location, extra: _self.$extra); +} + +mixin $SingboxProxyLogsRoute on GoRouteData { + static SingboxProxyLogsRoute _fromState(GoRouterState state) => + const SingboxProxyLogsRoute(); + + @override + String get location => + GoRouteData.$location('/settings/singbox_proxy_profiles/logs'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $SubscriptionImportRoute on GoRouteData { + static SubscriptionImportRoute _fromState(GoRouterState state) => + const SubscriptionImportRoute(); + + @override + String get location => + GoRouteData.$location('/settings/singbox_proxy_profiles/subscription'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $ProxyRoutingSettingsRoute on GoRouteData { + static ProxyRoutingSettingsRoute _fromState(GoRouterState state) => + const ProxyRoutingSettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/proxy_routing'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $ProxySettingsRoute on GoRouteData { + static ProxySettingsRoute _fromState(GoRouterState state) => + const ProxySettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/proxy'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + RouteBase get $torProxyRoute => GoRouteData.$route( path: '/tor', name: 'TorProxyRoute', diff --git a/apps/weblibre/lib/core/routing/routes.settings.dart b/apps/weblibre/lib/core/routing/routes.settings.dart index 05f7359a..feac1531 100644 --- a/apps/weblibre/lib/core/routing/routes.settings.dart +++ b/apps/weblibre/lib/core/routing/routes.settings.dart @@ -113,6 +113,29 @@ part of 'routes.dart'; name: 'ContextualToolbarSettingsRoute', path: 'contextual_toolbar', ), + TypedGoRoute( + name: 'SingboxProxyProfilesRoute', + path: 'singbox_proxy_profiles', + routes: [ + TypedGoRoute( + name: 'SingboxProxyProfileEditorRoute', + path: 'editor', + ), + TypedGoRoute( + name: 'SingboxProxyLogsRoute', + path: 'logs', + ), + TypedGoRoute( + name: 'SubscriptionImportRoute', + path: 'subscription', + ), + ], + ), + TypedGoRoute( + name: 'ProxyRoutingSettingsRoute', + path: 'proxy_routing', + ), + TypedGoRoute(name: 'ProxySettingsRoute', path: 'proxy'), ], ) class SettingsRoute extends GoRouteData with $SettingsRoute { @@ -315,3 +338,64 @@ class ContextualToolbarSettingsRoute extends GoRouteData return const ContextualToolbarSettingsScreen(); } } + +class SingboxProxyProfilesRoute extends GoRouteData + with $SingboxProxyProfilesRoute { + const SingboxProxyProfilesRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const SingboxProxyProfilesScreen(); + } +} + +class SingboxProxyProfileEditorRoute extends GoRouteData + with $SingboxProxyProfileEditorRoute { + final String? profileId; + final ProxyProfileSeed? $extra; + + const SingboxProxyProfileEditorRoute({this.profileId, this.$extra}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return SingboxProxyProfileEditorScreen(profileId: profileId, seed: $extra); + } +} + +class SingboxProxyLogsRoute extends GoRouteData with $SingboxProxyLogsRoute { + const SingboxProxyLogsRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const SingboxProxyLogsScreen(); + } +} + +class SubscriptionImportRoute extends GoRouteData + with $SubscriptionImportRoute { + const SubscriptionImportRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const SubscriptionImportScreen(); + } +} + +class ProxyRoutingSettingsRoute extends GoRouteData + with $ProxyRoutingSettingsRoute { + const ProxyRoutingSettingsRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const ProxyRoutingSettingsScreen(); + } +} + +class ProxySettingsRoute extends GoRouteData with $ProxySettingsRoute { + const ProxySettingsRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const ProxySettingsScreen(); + } +} diff --git a/apps/weblibre/lib/features/account/domain/services/handoff_redeem_client.dart b/apps/weblibre/lib/features/account/domain/services/handoff_redeem_client.dart index bf66043f..74db8d65 100644 --- a/apps/weblibre/lib/features/account/domain/services/handoff_redeem_client.dart +++ b/apps/weblibre/lib/features/account/domain/services/handoff_redeem_client.dart @@ -54,7 +54,8 @@ class HandoffRedeemResult { class HandoffRedeemClient { final http.Client _client; - HandoffRedeemClient({http.Client? client}) : _client = client ?? http.Client(); + HandoffRedeemClient({http.Client? client}) + : _client = client ?? http.Client(); void close() => _client.close(); diff --git a/apps/weblibre/lib/features/account/presentation/screens/account_settings.dart b/apps/weblibre/lib/features/account/presentation/screens/account_settings.dart index c6f3aa25..ca450591 100644 --- a/apps/weblibre/lib/features/account/presentation/screens/account_settings.dart +++ b/apps/weblibre/lib/features/account/presentation/screens/account_settings.dart @@ -106,7 +106,10 @@ class AccountSettingsScreen extends HookConsumerWidget { final syncRepo = syncClient != null ? ref.read(accountSyncRepositoryProvider.notifier) : null; - final sourceDeviceId = ref.read(androidDeviceInfoProvider).value?.deviceName; + final sourceDeviceId = ref + .read(androidDeviceInfoProvider) + .value + ?.deviceName; final sourceAppVersion = _appVersion(ref); return [ @@ -121,7 +124,8 @@ class AccountSettingsScreen extends HookConsumerWidget { AccountAuthStatus.error => 'Sign-in failed', }, subtitle: switch (authState.status) { - AccountAuthStatus.signedOut => 'Sync your settings across devices', + AccountAuthStatus.signedOut => + 'Sync your settings across devices', AccountAuthStatus.signingIn => 'Complete sign-in in your browser', AccountAuthStatus.signedIn => authState.displayName ?? authState.email ?? 'Signed in', diff --git a/apps/weblibre/lib/features/account/presentation/widgets/sync_document_list_section.dart b/apps/weblibre/lib/features/account/presentation/widgets/sync_document_list_section.dart index 41b62d3c..2bd980f9 100644 --- a/apps/weblibre/lib/features/account/presentation/widgets/sync_document_list_section.dart +++ b/apps/weblibre/lib/features/account/presentation/widgets/sync_document_list_section.dart @@ -23,10 +23,12 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:secure_archive/secure_archive.dart'; +import 'package:weblibre/core/logger.dart'; import 'package:weblibre/features/account/data/repositories/account_sync_repository.dart'; import 'package:weblibre/features/account/domain/services/sync_document_service.dart'; import 'package:weblibre/features/account/presentation/widgets/sync_document_dialogs.dart'; import 'package:weblibre/features/settings/presentation/widgets/settings_content_card.dart'; +import 'package:weblibre/utils/ui_helper.dart'; class SyncDocumentListSection extends HookWidget { const SyncDocumentListSection({ @@ -59,12 +61,18 @@ class SyncDocumentListSection extends HookWidget { Future refresh() async { try { final docs = await syncRepo.listDocuments(kind: service.kind); + + if (!context.mounted) return; + documents.value = docs; - } catch (e) { + } catch (error, stackTrace) { + logger.e( + 'Failed to list ${service.kind.name} sync documents', + error: error, + stackTrace: stackTrace, + ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to load snapshots: $e')), - ); + showErrorMessage(context, 'Failed to load snapshots: $error'); } } finally { if (context.mounted) { @@ -103,16 +111,17 @@ class SyncDocumentListSection extends HookWidget { ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${service.kind.displayName} stored')), - ); + showInfoMessage(context, '${service.kind.displayName} stored'); } await refresh(); - } catch (e) { + } catch (error, stackTrace) { + logger.e( + 'Failed to store ${service.kind.name} sync document', + error: error, + stackTrace: stackTrace, + ); if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Failed to store: $e'))); + showErrorMessage(context, 'Failed to store: $error'); } } finally { if (context.mounted) { @@ -133,9 +142,7 @@ class SyncDocumentListSection extends HookWidget { final result = await syncRepo.fetchDocument(id: metadata.id); if (result == null) { if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Snapshot not found'))); + showErrorMessage(context, 'Snapshot not found'); } return; } @@ -145,25 +152,23 @@ class SyncDocumentListSection extends HookWidget { await service.applyRestored(plaintext); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${service.kind.displayName} restored')), - ); + showInfoMessage(context, '${service.kind.displayName} restored'); } - } catch (e) { + } catch (error, stackTrace) { + logger.e( + 'Failed to restore ${service.kind.name} sync document ${metadata.id}', + error: error, + stackTrace: stackTrace, + ); if (context.mounted) { - if (_isDecryptionFailure(e)) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Decryption failed — wrong password or data corrupted. ' - 'Try resetting your sync key.', - ), - ), + if (_isDecryptionFailure(error)) { + showErrorMessage( + context, + 'Decryption failed — wrong password or data corrupted. ' + 'Try resetting your sync key.', ); } else { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Failed to restore: $e'))); + showErrorMessage(context, 'Failed to restore: $error'); } } } finally { @@ -186,11 +191,14 @@ class SyncDocumentListSection extends HookWidget { label: newLabel.isEmpty ? null : newLabel, ); await refresh(); - } catch (e) { + } catch (error, stackTrace) { + logger.e( + 'Failed to update label for sync document ${metadata.id}', + error: error, + stackTrace: stackTrace, + ); if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Failed to update label: $e'))); + showErrorMessage(context, 'Failed to update label: $error'); } } } @@ -205,16 +213,17 @@ class SyncDocumentListSection extends HookWidget { try { await syncRepo.deleteDocument(id: metadata.id); if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Snapshot deleted'))); + showInfoMessage(context, 'Snapshot deleted'); } await refresh(); - } catch (e) { + } catch (error, stackTrace) { + logger.e( + 'Failed to delete sync document ${metadata.id}', + error: error, + stackTrace: stackTrace, + ); if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Failed to delete: $e'))); + showErrorMessage(context, 'Failed to delete: $error'); } } } diff --git a/apps/weblibre/lib/features/account/presentation/widgets/sync_setup_card.dart b/apps/weblibre/lib/features/account/presentation/widgets/sync_setup_card.dart index 1ed08a00..142faffe 100644 --- a/apps/weblibre/lib/features/account/presentation/widgets/sync_setup_card.dart +++ b/apps/weblibre/lib/features/account/presentation/widgets/sync_setup_card.dart @@ -163,9 +163,7 @@ class SyncSetupCard extends HookConsumerWidget { Align( alignment: Alignment.centerRight, child: FilledButton( - onPressed: (busy.value || !passwordsMatch) - ? null - : setupSyncKey, + onPressed: (busy.value || !passwordsMatch) ? null : setupSyncKey, child: busy.value ? const SizedBox( height: 20, @@ -236,9 +234,7 @@ Future _uploadValidationProbe( String syncKey, ) async { final payload = utf8.encode('weblibre:sync-probe:v1'); - final secureData = SecureData( - argon2Params: Argon2Params.memoryConstrained(), - ); + final secureData = SecureData(argon2Params: Argon2Params.memoryConstrained()); final ciphertext = await secureData.encrypt(payload, syncKey); await syncRepo.storeDocument( kind: SyncDocumentKind.syncValidationProbe, diff --git a/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.dart b/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.dart index 053c39ba..a95a3c48 100644 --- a/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.dart +++ b/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.dart @@ -41,14 +41,13 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart' import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/utils/image_helper.dart'; -import 'package:weblibre/features/user/data/models/tor_settings.dart'; -import 'package:weblibre/features/user/domain/repositories/tor_settings.dart'; +import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart'; part 'tab_state.g.dart'; @Riverpod(keepAlive: true) class TabStates extends _$TabStates { - Future _onTabContentStateChange(TabContentState contentState) async { final current = await patchedState(contentState.id); @@ -377,25 +376,27 @@ Future tabStateWithFallback(Ref ref, String tabId) async { @Riverpod() Future isTabTunneled(Ref ref, String? tabId) async { final tabState = ref.watch(tabStateProvider(tabId)); - final torSettings = ref.watch(torSettingsWithDefaultsProvider); + final proxyRoutingSettings = ref.watch( + proxyRoutingSettingsWithDefaultsProvider, + ); if (tabState != null) { // Isolated tabs follow the same proxy rules as regular tabs // (container-based routing via proxy aliasing) if (tabState.tabMode is PrivateTabMode) { - return torSettings.proxyPrivateTabsTor; + return proxyRoutingSettings.privateTabsProxyConnectionId != null; } else { - switch (torSettings.proxyRegularTabsMode) { - case TorRegularTabProxyMode.container: + switch (proxyRoutingSettings.regularTabsMode) { + case ProxyRegularTabRoutingMode.container: final containerData = await ref .read(tabDataRepositoryProvider.notifier) .getTabContainerData(tabState.id); if (!ref.mounted) return false; - return containerData?.metadata.useProxy ?? false; - case TorRegularTabProxyMode.all: - return true; + return containerData?.metadata.proxyConnectionId != null; + case ProxyRegularTabRoutingMode.all: + return proxyRoutingSettings.regularTabsProxyConnectionId != null; } } } diff --git a/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.g.dart b/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.g.dart index db3d2e2a..c843a882 100644 --- a/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -258,7 +258,7 @@ final class IsTabTunneledProvider } } -String _$isTabTunneledHash() => r'b11134b33ad0c98ffddd14afc2c790fb65735e93'; +String _$isTabTunneledHash() => r'cb3a3afe5b94f7e9c77d6a32266afbb8a225c259'; final class IsTabTunneledFamily extends $Family with $FunctionalFamilyOverride, String?> { diff --git a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart index ca3b0cde..6ca04393 100644 --- a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart +++ b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart @@ -43,7 +43,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; -import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart'; +import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/utils/debouncer.dart'; @@ -417,7 +417,7 @@ class TabRepository extends _$TabRepository { if (!ref.mounted) return false; if (containerData != null) { - if (containerData.metadata.useProxy) { + if (containerData.metadata.proxyConnectionId != null) { final proxyPluginHealthy = await GeckoContainerProxyService() .healthcheck(); @@ -711,8 +711,8 @@ class TabRepository extends _$TabRepository { // Best-effort: remove proxy alias (no-op if never set) try { await ref - .read(torProxyRepositoryProvider.notifier) - .removeContainerProxy(contextId); + .read(containerProxyRepositoryProvider.notifier) + .clearContainerProxy(contextId); } catch (e, st) { logger.e( 'Failed to remove proxy for isolation context $contextId', diff --git a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart index 160441fd..8d57f5cb 100644 --- a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart @@ -41,7 +41,7 @@ final class TabRepositoryProvider } } -String _$tabRepositoryHash() => r'ea5caa3c9c4b19898db9de1f77d5476794b8b66f'; +String _$tabRepositoryHash() => r'2cd5b6bf5bb9a62c05b038b28acef59cecbeae4e'; abstract class _$TabRepository extends $Notifier { void build(); diff --git a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart index 74b90932..5ebc9622 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart @@ -18,41 +18,66 @@ * along with this program. If not, see . */ import 'package:fast_equatable/fast_equatable.dart'; -import 'package:nullability/nullability.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:synchronized/synchronized.dart'; import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; -import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; -import 'package:weblibre/features/user/data/models/tor_settings.dart'; -import 'package:weblibre/features/user/domain/repositories/tor_settings.dart'; +import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart'; part 'proxy_settings_replication.g.dart'; @Riverpod(keepAlive: true) class ProxySettingsReplication extends _$ProxySettingsReplication { - var _proxiedIsolationContexts = {}; + var _isolatedProxyAssignments = {}; + var _appliedContainerProxies = {}; final _recomputeLock = Lock(); + var _recomputeDirty = false; + + Future _ensureProxyConnectionAvailable( + ProxyConnectionId proxyConnectionId, + ) async { + if (proxyConnectionId is! SingboxProxyConnectionId) return; + + try { + await ref + .read(singboxProxyRuntimeRepositoryProvider.notifier) + .ensureProxyConnectionAvailable(proxyConnectionId); + } catch (error, stackTrace) { + logger.e( + 'Failed to make proxy connection $proxyConnectionId available; ' + 'assigned traffic will remain blocked', + error: error, + stackTrace: stackTrace, + ); + } + } Future _queueIsolatedProxyAliasesRecompute() async { - if (_recomputeLock.inLock) { - return; - } + _recomputeDirty = true; + if (_recomputeLock.inLock) return; await _recomputeLock.synchronized(() async { - try { - await _recomputeIsolatedProxyAliases(); - } catch (error, stackTrace) { - logger.e( - 'Error recomputing isolated proxy aliases', - error: error, - stackTrace: stackTrace, - ); + while (_recomputeDirty) { + _recomputeDirty = false; + try { + await _recomputeIsolatedProxyAliases(); + } catch (error, stackTrace) { + logger.e( + 'Error recomputing isolated proxy aliases', + error: error, + stackTrace: stackTrace, + ); + } } }); } @@ -78,37 +103,56 @@ class ProxySettingsReplication extends _$ProxySettingsReplication { .read(containerRepositoryProvider.notifier) .getAllContainersWithCount(); - // Build set of container IDs that have useProxy enabled. - final proxiedContainerIds = { + final containerProxyIds = { for (final c in containers) - if (c.metadata.useProxy) c.id, + if (c.metadata.proxyConnectionId case final proxyId?) + c.id: proxyId.encode(), }; - // Compute which isolation contexts need proxy aliases. - // A context needs an alias if ANY of its associated containers - // has useProxy enabled. - final newProxied = { - for (final entry in contextContainerMap.entries) - if (entry.value.any(proxiedContainerIds.contains)) entry.key, - }; + final newAssignments = {}; + for (final entry in contextContainerMap.entries) { + final proxyIds = + entry.value + .map((containerId) => containerProxyIds[containerId]) + .nonNulls + .toSet() + .toList() + ..sort(); - // Remove aliases that are no longer needed. - final toRemove = _proxiedIsolationContexts.difference(newProxied); + if (proxyIds.isEmpty) continue; + + newAssignments[entry.key] = proxyIds.first; + + if (proxyIds.length > 1) { + // Isolation contexts can hold multiple containers; if they disagree on + // a proxy connection the alias is forced to pick one. Surface this so + // the user can split the containers across isolation contexts. + logger.w( + 'Isolation context ${entry.key} has containers with multiple ' + 'proxy connections (${proxyIds.join(', ')}); using ${proxyIds.first}', + ); + } + } + + final previousContexts = _isolatedProxyAssignments.keys.toSet(); + final nextContexts = newAssignments.keys.toSet(); + + final toRemove = previousContexts.difference(nextContexts); for (final contextId in toRemove) { await ref - .read(torProxyRepositoryProvider.notifier) - .removeContainerProxy(contextId); + .read(containerProxyRepositoryProvider.notifier) + .clearContainerProxy(contextId); } - // Add aliases that are newly needed. - final toAdd = newProxied.difference(_proxiedIsolationContexts); - for (final contextId in toAdd) { + for (final MapEntry(:key, :value) in newAssignments.entries) { + if (_isolatedProxyAssignments[key] == value) continue; + await ref - .read(torProxyRepositoryProvider.notifier) - .addContainerProxy(contextId); + .read(containerProxyRepositoryProvider.notifier) + .setContainerProxy(key, value); } - _proxiedIsolationContexts = newProxied; + _isolatedProxyAssignments = newAssignments; } @override @@ -118,8 +162,8 @@ class ProxySettingsReplication extends _$ProxySettingsReplication { torProxyServiceProvider.select((data) => data.value), (previous, next) async { await ref - .read(torProxyRepositoryProvider.notifier) - .setProxyPort(next?.socksPort ?? -1); + .read(containerProxyRepositoryProvider.notifier) + .setTorProxyPort(next?.socksPort); }, onError: (error, stackTrace) { logger.e( @@ -132,51 +176,18 @@ class ProxySettingsReplication extends _$ProxySettingsReplication { ref.listen( fireImmediately: true, - watchContainersWithCountProvider.select( - (value) => EquatableValue(value.value), + proxyRoutingSettingsWithDefaultsProvider.select( + (value) => value.privateTabsProxyConnectionId, ), (previous, next) async { - if (next.value != null) { - for (final container in next.value!) { - if (container.metadata.contextualIdentity.isNotEmpty) { - if (container.metadata.useProxy) { - await ref - .read(torProxyRepositoryProvider.notifier) - .addContainerProxy(container.metadata.contextualIdentity!); - } else { - await ref - .read(torProxyRepositoryProvider.notifier) - .removeContainerProxy( - container.metadata.contextualIdentity!, - ); - } - } - } - } - }, - onError: (error, stackTrace) { - logger.e( - 'Error listening to containersWithCountProvider', - error: error, - stackTrace: stackTrace, + final containerProxy = ref.read( + containerProxyRepositoryProvider.notifier, ); - }, - ); - - ref.listen( - fireImmediately: true, - torSettingsWithDefaultsProvider.select( - (value) => value.proxyPrivateTabsTor, - ), - (previous, next) async { - if (next) { - await ref - .read(torProxyRepositoryProvider.notifier) - .addContainerProxy('private'); + if (next == null) { + await containerProxy.clearContainerProxy('private'); } else { - await ref - .read(torProxyRepositoryProvider.notifier) - .removeContainerProxy('private'); + await containerProxy.setContainerProxy('private', next.encode()); + await _ensureProxyConnectionAvailable(next); } }, onError: (error, stackTrace) { @@ -190,19 +201,27 @@ class ProxySettingsReplication extends _$ProxySettingsReplication { ref.listen( fireImmediately: true, - torSettingsWithDefaultsProvider.select( - (value) => value.proxyRegularTabsMode, + proxyRoutingSettingsWithDefaultsProvider.select( + (value) => (value.regularTabsMode, value.regularTabsProxyConnectionId), ), (previous, next) async { - switch (next) { - case TorRegularTabProxyMode.container: - await ref - .read(torProxyRepositoryProvider.notifier) - .removeContainerProxy('general'); - case TorRegularTabProxyMode.all: - await ref - .read(torProxyRepositoryProvider.notifier) - .addContainerProxy('general'); + final (regularTabsMode, proxyConnectionId) = next; + final containerProxy = ref.read( + containerProxyRepositoryProvider.notifier, + ); + switch (regularTabsMode) { + case ProxyRegularTabRoutingMode.container: + await containerProxy.clearContainerProxy('general'); + case ProxyRegularTabRoutingMode.all: + if (proxyConnectionId == null) { + await containerProxy.clearContainerProxy('general'); + } else { + await containerProxy.setContainerProxy( + 'general', + proxyConnectionId.encode(), + ); + await _ensureProxyConnectionAvailable(proxyConnectionId); + } } }, onError: (error, stackTrace) { @@ -217,7 +236,7 @@ class ProxySettingsReplication extends _$ProxySettingsReplication { ref.listen(watchAllAssignedSitesProvider, (previous, next) async { if (next.hasValue) { await ref - .read(torProxyRepositoryProvider.notifier) + .read(containerProxyRepositoryProvider.notifier) .setSiteAssignments(next.requireValue); } }); @@ -242,14 +261,80 @@ class ProxySettingsReplication extends _$ProxySettingsReplication { watchContainersWithCountProvider.select( (value) => EquatableValue(value.value), ), - (previous, next) => _queueIsolatedProxyAliasesRecompute(), + (previous, next) async { + await _applyContainerProxies(next.value); + await _queueIsolatedProxyAliasesRecompute(); + }, onError: (error, stackTrace) { logger.e( - 'Error listening to container proxy changes for isolated aliases', + 'Error listening to containersWithCountProvider', error: error, stackTrace: stackTrace, ); }, ); } + + /// Push per-container proxy assignments to Gecko, reconciling against the + /// last applied state so a transient mid-loop failure surfaces as a retry + /// on the next event rather than leaving Gecko half-updated forever. + Future _applyContainerProxies( + List? containers, + ) async { + if (containers == null) return; + + final desired = {}; + for (final container in containers) { + final contextId = container.metadata.contextualIdentity; + if (contextId == null || contextId.isEmpty) continue; + desired[contextId] = container.metadata.proxyConnectionId?.encode(); + } + + final repo = ref.read(containerProxyRepositoryProvider.notifier); + final nextApplied = Map.from(_appliedContainerProxies); + + for (final contextId in _appliedContainerProxies.keys.toSet().difference( + desired.keys.toSet(), + )) { + try { + await repo.clearContainerProxy(contextId); + nextApplied.remove(contextId); + } catch (error, stackTrace) { + logger.e( + 'Failed to clear removed container proxy assignment for $contextId', + error: error, + stackTrace: stackTrace, + ); + } + } + + for (final MapEntry(:key, :value) in desired.entries) { + if (_appliedContainerProxies[key] == value && + _appliedContainerProxies.containsKey(key)) { + continue; + } + try { + if (value != null) { + final proxyConnectionId = ProxyConnectionId.decode(value); + if (proxyConnectionId != null) { + await _ensureProxyConnectionAvailable(proxyConnectionId); + } + await repo.setContainerProxy(key, value); + } else { + await repo.clearContainerProxy(key); + } + nextApplied[key] = value; + } catch (error, stackTrace) { + logger.e( + 'Failed to push container proxy assignment for $key', + error: error, + stackTrace: stackTrace, + ); + // Leave nextApplied[key] at its previous value so the next event + // sees it as out-of-sync and retries. + } + } + + _appliedContainerProxies = nextApplied; + } } diff --git a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/proxy_settings_replication.g.dart b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/proxy_settings_replication.g.dart index 6957b4de..aee00658 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/proxy_settings_replication.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/proxy_settings_replication.g.dart @@ -42,7 +42,7 @@ final class ProxySettingsReplicationProvider } String _$proxySettingsReplicationHash() => - r'7f47a561441cc2d594e9380d62dcc31f4c1059d3'; + r'3a7f15c1e09e304d355dada2de80112dec0adbf4'; abstract class _$ProxySettingsReplication extends $Notifier { void build(); diff --git a/apps/weblibre/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart b/apps/weblibre/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart index e7e51efb..a1dddc18 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart @@ -877,13 +877,10 @@ Future _adjustFontSize( if (settings.automaticFontSizeAdjustment) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Disable automatic font size in settings to adjust manually', - ), - duration: Duration(seconds: 2), - ), + ui_helper.showInfoMessage( + context, + 'Disable automatic font size in settings to adjust manually', + duration: const Duration(seconds: 2), ); } return; diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 3942a208..8f3a5f06 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -58,6 +58,9 @@ import 'package:weblibre/features/geckoview/features/find_in_page/presentation/c import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; import 'package:weblibre/features/geckoview/features/search/domain/providers/search_autofocus.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/ensure_proxy_started.dart'; import 'package:weblibre/features/small_web/presentation/controllers/small_web_mode_controller.dart'; import 'package:weblibre/features/small_web/presentation/widgets/small_web_browser_overlay.dart'; import 'package:weblibre/features/sync/domain/repositories/sync.dart'; @@ -234,6 +237,55 @@ class BrowserScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final eventService = ref.watch(eventServiceProvider); final viewportService = ref.watch(viewportServiceProvider); + final activeProxyPromptKeys = useRef({}); + + useOnStreamChange( + eventService.proxyLoadErrorEvents, + onData: (event) async { + final selectedTabId = ref.read(selectedTabProvider); + final tabId = event.tabId ?? selectedTabId; + if (tabId == null || tabId != selectedTabId) return; + + final promptKey = '$tabId:${event.url ?? event.errorType}'; + if (!activeProxyPromptKeys.value.add(promptKey)) return; + + try { + final contextId = event.contextId; + final contextContainer = contextId != null && contextId.isNotEmpty + ? await ref + .read(containerRepositoryProvider.notifier) + .getContainerByContextualIdentity(contextId) + : null; + final container = + contextContainer ?? + await ref + .read(tabDataRepositoryProvider.notifier) + .getTabContainerData(tabId); + + if (!context.mounted || container == null) return; + + final isProxyStarted = await ensureProxyStartedForContainer( + context, + ref, + container, + ); + + if (isProxyStarted && + context.mounted && + ref.read(selectedTabProvider) == tabId) { + await ref.read(tabSessionProvider(tabId: tabId).notifier).reload(); + } + } catch (error, stackTrace) { + logger.e( + 'Failed to handle proxy load error', + error: error, + stackTrace: stackTrace, + ); + } finally { + activeProxyPromptKeys.value.remove(promptKey); + } + }, + ); final tabInFullScreen = ref.watch( selectedTabStateProvider.select((value) => value?.isFullScreen ?? false), diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart index 7b92f0b7..d86f3cc3 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart @@ -23,6 +23,7 @@ import 'dart:convert'; import 'dart:ui' as ui; import 'package:fading_scroll/fading_scroll.dart'; +import 'package:fast_equatable/fast_equatable.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -35,6 +36,7 @@ import 'package:nullability/nullability.dart'; import 'package:share_plus/share_plus.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:weblibre/core/design/app_colors.dart'; +import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/providers/persisted_bool.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; @@ -67,10 +69,14 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart' import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart'; +import 'package:weblibre/features/proxy/data/models/singbox_proxy_profile.dart'; +import 'package:weblibre/features/proxy/domain/providers/assigned_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; import 'package:weblibre/features/small_web/presentation/controllers/small_web_mode_controller.dart'; import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart'; import 'package:weblibre/features/sync/domain/repositories/sync.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; +import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart'; import 'package:weblibre/features/user/domain/providers.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; @@ -1930,102 +1936,162 @@ class _QuickLinksGrid extends ConsumerWidget { final isTorActive = ref.watch( torProxyServiceProvider.select((value) => value.value?.isRunning == true), ); + final isTorBusy = ref.watch(startProxyControllerProvider); - return Column( - children: [ - Row( - children: [ - Expanded( - child: _buildGridItem( - context, - Icons.history, - 'History', - () async { - Navigator.pop(context); - await const HistoryRoute().push(context); - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildGridItem( - context, - MdiIcons.bookmarkMultiple, - 'Bookmarks', - () async { - Navigator.pop(context); - await BookmarkListRoute( - entryGuid: BookmarkRoot.root.id, - ).push(context); - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildGridItem( - context, - MdiIcons.fileDownload, - 'Downloads', - () async { - Navigator.pop(context); - await const HistoryDownloadsRoute().push(context); - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildGridItem( - context, - MdiIcons.exclamationThick, - 'Bangs', - () async { - Navigator.pop(context); - await const BangMenuRoute().push(context); - }, - ), - ), - ], + final assignedProfiles = ref.watch(assignedSingboxProxyProfilesProvider); + final runtimeEndpointIds = ref + .watch( + singboxProxyRuntimeRepositoryProvider.select((value) { + final endpoints = value.value?.endpoints; + if (endpoints == null) { + return EquatableValue(const {}); + } + + return EquatableValue({ + for (final endpoint in endpoints) endpoint.profileId, + }); + }), + ) + .value; + + final torActiveColor = AppColors.of(context).torActiveGreen; + + final items = <_QuickLinkItem>[ + _QuickLinkItem( + icon: Icons.history, + label: 'History', + onTap: () async { + Navigator.pop(context); + await const HistoryRoute().push(context); + }, + ), + _QuickLinkItem( + icon: MdiIcons.bookmarkMultiple, + label: 'Bookmarks', + onTap: () async { + Navigator.pop(context); + await BookmarkListRoute( + entryGuid: BookmarkRoot.root.id, + ).push(context); + }, + ), + _QuickLinkItem( + icon: MdiIcons.fileDownload, + label: 'Downloads', + onTap: () async { + Navigator.pop(context); + await const HistoryDownloadsRoute().push(context); + }, + ), + _QuickLinkItem( + icon: MdiIcons.exclamationThick, + label: 'Bangs', + onTap: () async { + Navigator.pop(context); + await const BangMenuRoute().push(context); + }, + ), + _QuickLinkItem( + icon: Icons.rss_feed, + label: 'Feeds', + onTap: () async { + Navigator.pop(context); + await context.push(FeedListRoute().location); + }, + ), + _QuickLinkItem( + icon: Icons.explore, + label: 'Small Web', + onTap: () async { + Navigator.pop(context); + await ref.read(smallWebModeControllerProvider.notifier).enter(); + }, + ), + _QuickLinkItem( + icon: TorIcons.onionAlt, + label: 'Tor\u2122 Proxy', + badge: isTorActive, + badgeColor: torActiveColor, + onTap: () async { + if (isTorBusy) return; + if (isTorActive) { + await ref.read(torProxyServiceProvider.notifier).disconnect(); + } else { + await ref.read(startProxyControllerProvider.notifier).startProxy(); + } + }, + onLongPress: () async { + Navigator.pop(context); + await const TorProxyRoute().push(context); + }, + ), + for (final profile in assignedProfiles) + _QuickLinkItem( + icon: MdiIcons.lanConnect, + label: profile.name, + badge: runtimeEndpointIds.contains(profile.proxyConnectionId), + badgeColor: torActiveColor, + onTap: () async { + final runtime = ref.read( + singboxProxyRuntimeRepositoryProvider.notifier, + ); + final isRunning = runtimeEndpointIds.contains( + profile.proxyConnectionId, + ); + + try { + if (isRunning) { + await runtime.stopProfiles([profile.id]); + } else { + await runtime.startProfile(profile.id); + } + } catch (error, stackTrace) { + logger.e( + 'Failed to toggle proxy profile ${profile.id} from menu', + error: error, + stackTrace: stackTrace, + ); + if (context.mounted) { + ui_helper.showErrorMessage(context, 'Proxy error: $error'); + } + } + }, + onLongPress: () async { + Navigator.pop(context); + await SingboxProxyProfileEditorRoute( + profileId: profile.id, + ).push(context); + }, ), - const SizedBox(height: 8), - Row( + ]; + + const spacing = 8.0; + const itemsPerRow = 4; + + return LayoutBuilder( + builder: (context, constraints) { + final width = + (constraints.maxWidth - spacing * (itemsPerRow - 1)) / itemsPerRow; + return Wrap( + spacing: spacing, + runSpacing: spacing, children: [ - Expanded( - child: _buildGridItem( - context, - TorIcons.onionAlt, - 'Tor\u2122 Proxy', - () async { - Navigator.pop(context); - await const TorProxyRoute().push(context); - }, - badge: isTorActive, - badgeColor: AppColors.of(context).torActiveGreen, + for (final item in items) + SizedBox( + width: width, + child: _buildGridItem( + context, + item.icon, + item.label, + item.onTap, + badge: item.badge, + badgeColor: item.badgeColor, + onLongPress: item.onLongPress, + ), ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildGridItem(context, Icons.rss_feed, 'Feeds', () async { - Navigator.pop(context); - await context.push(FeedListRoute().location); - }), - ), - const SizedBox(width: 8), - Expanded( - child: _buildGridItem( - context, - Icons.explore, - 'Small Web', - () async { - Navigator.pop(context); - await ref - .read(smallWebModeControllerProvider.notifier) - .enter(); - }, - ), - ), ], - ), - ], + ); + }, ); } @@ -2036,6 +2102,7 @@ class _QuickLinksGrid extends ConsumerWidget { VoidCallback onTap, { bool badge = false, Color? badgeColor, + VoidCallback? onLongPress, }) { final iconWidget = Icon( icon, @@ -2048,6 +2115,7 @@ class _QuickLinksGrid extends ConsumerWidget { clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, + onLongPress: onLongPress, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), @@ -2075,6 +2143,24 @@ class _QuickLinksGrid extends ConsumerWidget { } } +class _QuickLinkItem { + final IconData icon; + final String label; + final VoidCallback onTap; + final VoidCallback? onLongPress; + final bool badge; + final Color? badgeColor; + + const _QuickLinkItem({ + required this.icon, + required this.label, + required this.onTap, + this.onLongPress, + this.badge = false, + this.badgeColor, + }); +} + // ─── Profile Card ─── class _ProfileCard extends HookConsumerWidget { diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart index d75014e7..c7262813 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart @@ -50,10 +50,10 @@ import 'package:weblibre/features/geckoview/features/history/domain/repositories import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart'; import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart'; -import 'package:weblibre/features/geckoview/features/tabs/domain/services/local_index_pruner.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/services/local_index_pruner.dart'; import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart'; import 'package:weblibre/features/intent_gatekeeper/domain/entities/pending_intent_decision.dart'; import 'package:weblibre/features/intent_gatekeeper/domain/services/intent_gatekeeper.dart'; @@ -636,15 +636,15 @@ class _BrowserViewState extends ConsumerState }, ); - //Ensure tor events don't get dropped + // Keep-alive subscription for Tor status. The body is intentionally empty: + // the listener exists so the provider stays active while the browser view + // is mounted and Tor state events are not dropped. singboxProxyLogs is + // kept alive from main.dart so startup logs are captured even before the + // browser view mounts. ref.listenManual( fireImmediately: true, torProxyServiceProvider, - (previous, next) { - if (next.hasValue) { - debugPrint(next.requireValue.toString()); - } - }, + (previous, next) {}, onError: (error, stackTrace) { logger.e( 'Error listening to torProxyServiceProvider', diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart index c8985525..f41cf16d 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart @@ -73,6 +73,9 @@ class ViewTabSheetWidget extends HookConsumerWidget { useEffect( () { WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; + if (!draggableScrollableController.isAttached) return; + final diff = ((MediaQuery.of(context).viewInsets.bottom / 2) / MediaQuery.of(context).size.height) - diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart index 77c7d266..6c3b7155 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart @@ -51,9 +51,8 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selec import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/ensure_proxy_started.dart'; import 'package:weblibre/features/sync/domain/repositories/sync.dart'; -import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; -import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/widgets/speech_to_text_button.dart'; @@ -111,26 +110,8 @@ class _TabFilters extends ConsumerWidget { .read(selectedContainerProvider.notifier) .setContainerId(container.id); - if (context.mounted && - result == SetContainerResult.successHasProxy) { - final shouldStartProxy = await ref - .read(startProxyControllerProvider.notifier) - .shouldPromptProxyStart(); - - if (!context.mounted || !shouldStartProxy) return; - - final dialogResult = await showDialog( - context: context, - builder: (context) { - return const TorDialog(); - }, - ); - - if (dialogResult == true) { - await ref - .read(startProxyControllerProvider.notifier) - .startProxy(); - } + if (context.mounted && result == SetContainerResult.success) { + await ensureProxyStartedForContainer(context, ref, container); } } else { ref.read(selectedContainerProvider.notifier).clearContainer(); diff --git a/apps/weblibre/lib/features/geckoview/features/search/domain/providers/combined_history.dart b/apps/weblibre/lib/features/geckoview/features/search/domain/providers/combined_history.dart index 2081133d..5feeee1b 100644 --- a/apps/weblibre/lib/features/geckoview/features/search/domain/providers/combined_history.dart +++ b/apps/weblibre/lib/features/geckoview/features/search/domain/providers/combined_history.dart @@ -73,9 +73,9 @@ List combinedHistorySuggestions(Ref ref) { final engineAsync = ref.watch(engineSuggestionsProvider); final localAsync = ref.watch(historySearchRepositoryProvider); - final engineSuggestions = - engineAsync.value ?? const []; - final localResults = localAsync.value?.results ?? const []; + final engineSuggestions = engineAsync.value ?? const []; + final localResults = + localAsync.value?.results ?? const []; // Index the local rows by canonical URL so engine items can pick up // snippet/title-highlight without an N×M scan. diff --git a/apps/weblibre/lib/features/geckoview/features/search/presentation/screens/search.dart b/apps/weblibre/lib/features/geckoview/features/search/presentation/screens/search.dart index 64a24a19..d17243cc 100644 --- a/apps/weblibre/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/apps/weblibre/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -53,8 +53,8 @@ import 'package:weblibre/features/geckoview/features/search/presentation/widgets import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_field.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_module_reorder_view.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/bookmark_search.dart'; -import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/feed_search.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/combined_history_suggestions.dart'; +import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/feed_search.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/local_history_suggestions.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/search_providers_section.dart'; @@ -64,8 +64,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolatio import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/compact_container_selector.dart'; -import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; -import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/ensure_proxy_started.dart'; import 'package:weblibre/features/search_credits/domain/repositories/web_search_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/web_search/domain/controllers/search_controller.dart'; @@ -496,26 +495,11 @@ class SearchScreen extends HookConsumerWidget { if (!context.mounted) return; - if (result == SetContainerResult.successHasProxy) { - final shouldStartProxy = await ref - .read(startProxyControllerProvider.notifier) - .shouldPromptProxyStart(); - - if (context.mounted && shouldStartProxy) { - final dialogResult = await showDialog( - context: context, - builder: (_) => const TorDialog(), - ); - - if (dialogResult == true) { - await ref - .read(startProxyControllerProvider.notifier) - .startProxy(); - } - } + if (result == SetContainerResult.success) { + await ensureProxyStartedForContainer(context, ref, container); } - if (context.mounted && result != SetContainerResult.failed) { + if (context.mounted && result == SetContainerResult.success) { const TabViewRoute().go(context); } }, diff --git a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/combined_history_suggestions.dart b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/combined_history_suggestions.dart index 0b50ffc6..02f3972f 100644 --- a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/combined_history_suggestions.dart +++ b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/combined_history_suggestions.dart @@ -75,9 +75,7 @@ class CombinedHistorySuggestions extends HookConsumerWidget { // which is the same constant the row builder scans for below. // Don't override the defaults here unless those scan constants // are kept in sync. - await ref - .read(historySearchRepositoryProvider.notifier) - .addQuery(text); + await ref.read(historySearchRepositoryProvider.notifier).addQuery(text); }, ); diff --git a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart index 7ba7fecd..0c17f696 100644 --- a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart +++ b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart @@ -97,8 +97,10 @@ class HistorySuggestions extends HookConsumerWidget { ), subtitle: uri.mapNotNull( - (uri) => - UriBreadcrumb(uri: uri, showHttpScheme: false), + (uri) => UriBreadcrumb( + uri: uri, + showHttpScheme: false, + ), ) ?? suggestion.description.mapNotNull( (description) => Text( diff --git a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/local_history_suggestions.dart b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/local_history_suggestions.dart index 76c99d82..b5c05960 100644 --- a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/local_history_suggestions.dart +++ b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/local_history_suggestions.dart @@ -83,12 +83,15 @@ class LocalHistorySuggestions extends HookConsumerWidget { final uri = Uri.tryParse(result.urlCanonical); final content = - (result.extractedContent?.contains(historyHighlightPrefix) == - true) - ? result.extractedContent - : result.fullContent; + (result.extractedContent?.contains( + historyHighlightPrefix, + ) == + true) + ? result.extractedContent + : result.fullContent; final titleHasMatch = - result.title?.contains(historyHighlightPrefix) ?? false; + result.title?.contains(historyHighlightPrefix) ?? + false; final bodyHasMatch = content?.contains(historyHighlightPrefix) ?? false; diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.dart index 4a5e539e..219f616c 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.dart @@ -23,6 +23,7 @@ import 'package:flutter/widgets.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:weblibre/data/database/converters/color.dart'; import 'package:weblibre/data/database/converters/icon_data.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; part 'container_data.g.dart'; @@ -31,10 +32,14 @@ part 'container_data.g.dart'; class ContainerMetadata with FastEquatable { @IconDataJsonConverter() final IconData? iconData; + final String? contextualIdentity; - @JsonKey(defaultValue: false) - final bool useProxy; + @JsonKey( + fromJson: _proxyConnectionIdFromJson, + toJson: _proxyConnectionIdToJson, + ) + final ProxyConnectionId? proxyConnectionId; @JsonKey(defaultValue: false) final bool clearDataOnExit; @@ -44,7 +49,7 @@ class ContainerMetadata with FastEquatable { ContainerMetadata({ required this.iconData, required this.contextualIdentity, - required this.useProxy, + required this.proxyConnectionId, required this.clearDataOnExit, required this.assignedSites, }); @@ -52,17 +57,19 @@ class ContainerMetadata with FastEquatable { ContainerMetadata.withDefaults({ IconData? iconData, String? contextualIdentity, - bool? useProxy, + ProxyConnectionId? proxyConnectionId, bool? clearDataOnExit, List? assignedSites, }) : this( iconData: iconData, contextualIdentity: contextualIdentity, - useProxy: useProxy ?? false, + proxyConnectionId: proxyConnectionId, clearDataOnExit: clearDataOnExit ?? false, assignedSites: assignedSites, ); + bool get usesTorProxy => proxyConnectionId is TorProxyConnectionId; + factory ContainerMetadata.fromJson(Map json) => _$ContainerMetadataFromJson(json); @@ -72,7 +79,7 @@ class ContainerMetadata with FastEquatable { List get hashParameters => [ iconData, contextualIdentity, - useProxy, + proxyConnectionId, clearDataOnExit, assignedSites, ]; @@ -141,3 +148,8 @@ class ContainerDataWithCount extends ContainerData { @override List get hashParameters => [...super.hashParameters, tabCount]; } + +ProxyConnectionId? _proxyConnectionIdFromJson(String? json) => + ProxyConnectionId.decode(json); + +String? _proxyConnectionIdToJson(ProxyConnectionId? object) => object?.encode(); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.g.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.g.dart index c088faa9..808964f6 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.g.dart @@ -11,7 +11,7 @@ abstract class _$ContainerMetadataCWProxy { ContainerMetadata contextualIdentity(String? contextualIdentity); - ContainerMetadata useProxy(bool useProxy); + ContainerMetadata proxyConnectionId(ProxyConnectionId? proxyConnectionId); ContainerMetadata clearDataOnExit(bool clearDataOnExit); @@ -27,7 +27,7 @@ abstract class _$ContainerMetadataCWProxy { ContainerMetadata call({ IconData? iconData, String? contextualIdentity, - bool useProxy, + ProxyConnectionId? proxyConnectionId, bool clearDataOnExit, List? assignedSites, }); @@ -48,7 +48,8 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy { call(contextualIdentity: contextualIdentity); @override - ContainerMetadata useProxy(bool useProxy) => call(useProxy: useProxy); + ContainerMetadata proxyConnectionId(ProxyConnectionId? proxyConnectionId) => + call(proxyConnectionId: proxyConnectionId); @override ContainerMetadata clearDataOnExit(bool clearDataOnExit) => @@ -69,7 +70,7 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy { ContainerMetadata call({ Object? iconData = const $CopyWithPlaceholder(), Object? contextualIdentity = const $CopyWithPlaceholder(), - Object? useProxy = const $CopyWithPlaceholder(), + Object? proxyConnectionId = const $CopyWithPlaceholder(), Object? clearDataOnExit = const $CopyWithPlaceholder(), Object? assignedSites = const $CopyWithPlaceholder(), }) { @@ -82,10 +83,10 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy { ? _value.contextualIdentity // ignore: cast_nullable_to_non_nullable : contextualIdentity as String?, - useProxy: useProxy == const $CopyWithPlaceholder() || useProxy == null - ? _value.useProxy + proxyConnectionId: proxyConnectionId == const $CopyWithPlaceholder() + ? _value.proxyConnectionId // ignore: cast_nullable_to_non_nullable - : useProxy as bool, + : proxyConnectionId as ProxyConnectionId?, clearDataOnExit: clearDataOnExit == const $CopyWithPlaceholder() || clearDataOnExit == null @@ -227,7 +228,9 @@ ContainerMetadata _$ContainerMetadataFromJson(Map json) => const IconDataJsonConverter().fromJson, ), contextualIdentity: json['contextualIdentity'] as String?, - useProxy: json['useProxy'] as bool? ?? false, + proxyConnectionId: _proxyConnectionIdFromJson( + json['proxyConnectionId'] as String?, + ), clearDataOnExit: json['clearDataOnExit'] as bool? ?? false, assignedSites: (json['assignedSites'] as List?) ?.map((e) => Uri.parse(e as String)) @@ -242,7 +245,7 @@ Map _$ContainerMetadataToJson( const IconDataJsonConverter().toJson, ), 'contextualIdentity': instance.contextualIdentity, - 'useProxy': instance.useProxy, + 'proxyConnectionId': _proxyConnectionIdToJson(instance.proxyConnectionId), 'clearDataOnExit': instance.clearDataOnExit, 'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(), }; diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.dart index 819b52ec..fdc67093 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.dart @@ -190,7 +190,7 @@ Stream> watchAllAssignedSites(Ref ref) { /// /// Returns a map from isolation context ID to the set of container IDs it /// appears in. An isolation context needs a proxy alias if ANY of its -/// associated containers has useProxy enabled. +/// associated containers has a proxy connection assigned. @Riverpod(keepAlive: true) Stream>> watchIsolatedContextContainerMap(Ref ref) { final db = ref.watch(tabDatabaseProvider); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.g.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.g.dart index 67e953ca..a2139e4d 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.g.dart @@ -1082,7 +1082,7 @@ String _$watchAllAssignedSitesHash() => /// /// Returns a map from isolation context ID to the set of container IDs it /// appears in. An isolation context needs a proxy alias if ANY of its -/// associated containers has useProxy enabled. +/// associated containers has a proxy connection assigned. @ProviderFor(watchIsolatedContextContainerMap) final watchIsolatedContextContainerMapProvider = @@ -1094,7 +1094,7 @@ final watchIsolatedContextContainerMapProvider = /// /// Returns a map from isolation context ID to the set of container IDs it /// appears in. An isolation context needs a proxy alias if ANY of its -/// associated containers has useProxy enabled. +/// associated containers has a proxy connection assigned. final class WatchIsolatedContextContainerMapProvider extends @@ -1112,7 +1112,7 @@ final class WatchIsolatedContextContainerMapProvider /// /// Returns a map from isolation context ID to the set of container IDs it /// appears in. An isolation context needs a proxy alias if ANY of its - /// associated containers has useProxy enabled. + /// associated containers has a proxy connection assigned. WatchIsolatedContextContainerMapProvider._() : super( from: null, diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart index 070d7430..0a5ad8f1 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart @@ -37,7 +37,7 @@ import 'package:weblibre/features/user/data/providers.dart'; part 'selected_container.g.dart'; -enum SetContainerResult { failed, success, successHasProxy } +enum SetContainerResult { failed, success } @Riverpod(keepAlive: true) class SelectedContainer extends _$SelectedContainer { @@ -62,13 +62,13 @@ class SelectedContainer extends _$SelectedContainer { bool canApply() => shouldApply?.call() ?? true; if (ref.mounted && container != null && canApply()) { - if (container.metadata.useProxy) { + if (container.metadata.proxyConnectionId != null) { final proxyPluginHealthy = await GeckoContainerProxyService() .healthcheck(); if (ref.mounted && proxyPluginHealthy && canApply()) { state = id; - return SetContainerResult.successHasProxy; + return SetContainerResult.success; } } else if (canApply()) { state = id; diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart index a92614f4..de305613 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart @@ -41,7 +41,7 @@ final class SelectedContainerProvider } } -String _$selectedContainerHash() => r'1f0828ce1d3f8b88fd2731a7aae2602a7323092c'; +String _$selectedContainerHash() => r'3c9ab093bce7cd3b84e0318095b1ada25bdf8a65'; abstract class _$SelectedContainer extends $Notifier { String? build(); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/container.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/container.dart index 1f97a88f..00f0d077 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/container.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/container.dart @@ -28,6 +28,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assig import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/color_palette.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; part 'container.g.dart'; @@ -78,6 +79,28 @@ class ContainerRepository extends _$ContainerRepository { .replaceContainer(container); } + Future clearProxyConnectionAssignments( + ProxyConnectionId proxyConnectionId, + ) async { + final containers = await getAllContainersWithCount(); + final affectedContainers = containers.where( + (container) => container.metadata.proxyConnectionId == proxyConnectionId, + ); + + for (final container in affectedContainers) { + await replaceContainer( + ContainerData( + id: container.id, + name: container.name, + color: container.color, + orderKey: container.orderKey, + isPinned: container.isPinned, + metadata: container.metadata.copyWith.proxyConnectionId(null), + ), + ); + } + } + Future assignContainerOrderKey(String id, {required String orderKey}) { return ref .read(tabDatabaseProvider) @@ -214,17 +237,25 @@ class ContainerRepository extends _$ContainerRepository { .take(targetIndex) .where((container) => container.isPinned == movingContainer.isPinned) .length - .clamp(0, scopedContainers.length - 1) - .toInt(); + .clamp(0, scopedContainers.length - 1); if (scopedTargetIndex == scopedOldIndex) return; - final orderKey = await _orderKeyForReorder( - scopedContainers, - scopedOldIndex, - scopedTargetIndex, - movingContainer.isPinned, - ); - await assignContainerOrderKey(movingContainer.id, orderKey: orderKey); + // Generate a new order key and assign it in the same transaction so a + // crash between the two cannot leave the moving container with a stale + // key that no longer matches the surrounding rows. + final db = ref.read(tabDatabaseProvider); + await db.transaction(() async { + final orderKey = await _orderKeyForReorder( + scopedContainers, + scopedOldIndex, + scopedTargetIndex, + movingContainer.isPinned, + ); + await db.containerDao.assignOrderKey( + movingContainer.id, + orderKey: orderKey, + ); + }); } Future _orderKeyForReorder( diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart index 8717e89e..fc6db9ac 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart @@ -42,7 +42,7 @@ final class ContainerRepositoryProvider } String _$containerRepositoryHash() => - r'a708f298d7bd50f1823029e00f8ed295b9f81f4e'; + r'8db0f2a08ee5a67123f9c110ef7edf4c574992c4'; abstract class _$ContainerRepository extends $Notifier { void build(); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/history_search.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/history_search.dart index 1b8a69c9..eef7fedc 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/history_search.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/history_search.dart @@ -200,9 +200,7 @@ class HistorySearchRepository extends _$HistorySearchRepository { // last time the engine observed *anything* about this URL. final updatedAt = DateTime.fromMillisecondsSinceEpoch(metadata.updatedAt); final ageDays = now.difference(updatedAt).inHours / 24.0; - final decay = math.exp( - -math.max(0.0, ageDays) / _recencyHalfLifeDays, - ); + final decay = math.exp(-math.max(0.0, ageDays) / _recencyHalfLifeDays); score -= _recencyRankWeight * decay; } diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/services/local_index_settings_sync.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/services/local_index_settings_sync.dart index f1031994..28acd9cd 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/services/local_index_settings_sync.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/services/local_index_settings_sync.dart @@ -55,14 +55,12 @@ class LocalIndexSettingsSync extends _$LocalIndexSettingsSync { /// last click. The lock is fine-grained per provider instance. final _writeLock = Lock(); - Future _push({ - required bool enabled, - required bool indexPrivate, - }) => _writeLock.synchronized(() async { - final dao = ref.read(tabDatabaseProvider).historyDao; - await dao.upsertSetting(_kEnabledKey, enabled); - await dao.upsertSetting(_kIndexPrivateKey, indexPrivate); - }); + Future _push({required bool enabled, required bool indexPrivate}) => + _writeLock.synchronized(() async { + final dao = ref.read(tabDatabaseProvider).historyDao; + await dao.upsertSetting(_kEnabledKey, enabled); + await dao.upsertSetting(_kIndexPrivateKey, indexPrivate); + }); @override void build() { @@ -77,7 +75,9 @@ class LocalIndexSettingsSync extends _$LocalIndexSettingsSync { if (previous == next) return; // Fire-and-forget: `_writeLock` ensures the second flip queues // behind the first instead of racing it. - unawaited(_push(enabled: next.enabled, indexPrivate: next.indexPrivate)); + unawaited( + _push(enabled: next.enabled, indexPrivate: next.indexPrivate), + ); }, fireImmediately: true, ); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart index 8c73c3b3..4bf16c31 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart @@ -23,7 +23,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:nullability/nullability.dart'; import 'package:weblibre/core/uuid.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; @@ -31,11 +30,12 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/controlle import 'package:weblibre/features/geckoview/features/tabs/presentation/dialogs/delete_container_dialog.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/dialogs/discard_changes_dialog.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_sites.dart'; -import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_icon_picker_sheet.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart'; +import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_icon_picker_sheet.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/container_icons.dart'; -import 'package:weblibre/presentation/icons/tor_icons.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart'; enum _DialogMode { create, edit } @@ -77,12 +77,16 @@ class ContainerEditScreen extends HookConsumerWidget { final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final proxyOptions = ref.watch(proxyConnectionOptionsProvider); + final selectedColor = useState(initialContainer.color); final selectedIcon = useState(initialContainer.metadata.iconData); final contextualIdentity = useState( initialContainer.metadata.contextualIdentity, ); - final useProxy = useState(initialContainer.metadata.useProxy); + final proxyConnectionId = useState( + initialContainer.metadata.proxyConnectionId, + ); final clearDataOnExit = useState(initialContainer.metadata.clearDataOnExit); final assignedSites = useState(initialContainer.metadata.assignedSites); @@ -99,7 +103,9 @@ class ContainerEditScreen extends HookConsumerWidget { metadata: initialContainer.metadata.copyWith( contextualIdentity: contextualIdentity.value, iconData: selectedIcon.value, - useProxy: useProxy.value && contextualIdentity.value != null, + proxyConnectionId: contextualIdentity.value != null + ? proxyConnectionId.value + : null, clearDataOnExit: clearDataOnExit.value && contextualIdentity.value != null, assignedSites: assignedSites.value, @@ -217,6 +223,8 @@ class ContainerEditScreen extends HookConsumerWidget { selectedColor.value, ); final assignedSiteCount = assignedSites.value?.length ?? 0; + final canPickProxy = + _mode == _DialogMode.create || contextualIdentity.value != null; return PopScope( canPop: container == comparison, @@ -358,8 +366,8 @@ class ContainerEditScreen extends HookConsumerWidget { uuid.v4() : null; - if (!value && useProxy.value) { - useProxy.value = false; + if (!value) { + proxyConnectionId.value = null; } if (!value && clearDataOnExit.value) { @@ -369,29 +377,61 @@ class ContainerEditScreen extends HookConsumerWidget { : null, ), const Divider(height: 1, indent: 56), - SwitchListTile.adaptive( - value: useProxy.value, - title: const Text('Use Tor™ Proxy'), - secondary: const Icon(TorIcons.onionAlt), - onChanged: switch (_mode) { - _DialogMode.create => (value) { - if (value && contextualIdentity.value == null) { - contextualIdentity.value = - initialContainer - .metadata - .contextualIdentity ?? - uuid.v4(); - } + ListTile( + leading: const Icon(Icons.route_outlined), + title: const Text('Proxy Connection'), + subtitle: Text(switch (proxyConnectionId.value) { + final id? => proxyConnectionTitle(proxyOptions, id), + null => 'None', + }), + trailing: const Icon(Icons.chevron_right), + enabled: canPickProxy, + onTap: canPickProxy + ? () async { + final createdTemporaryIdentity = + contextualIdentity.value == null; - useProxy.value = value; - }, - _DialogMode.edit => - (contextualIdentity.value != null) - ? (value) { - useProxy.value = value; - } - : null, - }, + if (createdTemporaryIdentity) { + contextualIdentity.value = + initialContainer + .metadata + .contextualIdentity ?? + uuid.v4(); + } + + final outcome = + await showModalBottomSheet< + _ProxyPickerOutcome + >( + context: context, + showDragHandle: true, + builder: (context) { + return _ProxyConnectionPickerSheet( + options: proxyOptions, + selectedProxyConnectionId: + proxyConnectionId.value, + ); + }, + ); + + switch (outcome) { + case null: + // Dismissed without selecting — leave + // existing value untouched, but undo any + // temporary identity we created. + if (createdTemporaryIdentity) { + contextualIdentity.value = null; + } + case _ProxyPickerCleared(): + proxyConnectionId.value = null; + if (createdTemporaryIdentity) { + contextualIdentity.value = null; + } + case _ProxyPickerSelected(:final id): + proxyConnectionId.value = id; + } + } + : null, ), const Divider(height: 1, indent: 56), SwitchListTile.adaptive( @@ -524,3 +564,90 @@ class ContainerEditScreen extends HookConsumerWidget { ); } } + +/// Result of the proxy picker sheet. `null` (sheet dismissed) is distinct +/// from `_ProxyPickerCleared` (user explicitly picked None) so the caller can +/// avoid clobbering the previously-selected proxy on a stray swipe-down. +sealed class _ProxyPickerOutcome { + const _ProxyPickerOutcome(); +} + +class _ProxyPickerCleared extends _ProxyPickerOutcome { + const _ProxyPickerCleared(); +} + +class _ProxyPickerSelected extends _ProxyPickerOutcome { + final ProxyConnectionId id; + + const _ProxyPickerSelected(this.id); +} + +class _ProxyConnectionPickerSheet extends StatelessWidget { + final List options; + final ProxyConnectionId? selectedProxyConnectionId; + + const _ProxyConnectionPickerSheet({ + required this.options, + required this.selectedProxyConnectionId, + }); + + @override + Widget build(BuildContext context) { + final hasUnknownSelectedProxy = + selectedProxyConnectionId != null && + !options.any((option) => option.id == selectedProxyConnectionId); + + return SafeArea( + child: RadioGroup( + onChanged: (value) { + Navigator.pop( + context, + value == null + ? const _ProxyPickerCleared() + : _ProxyPickerSelected(value), + ); + }, + child: ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 4, 24, 12), + child: Text( + 'Proxy Connection', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + const RadioListTile( + value: null, + title: Text('None'), + subtitle: Text('Use the normal browser connection'), + secondary: Icon(Icons.public), + ), + if (hasUnknownSelectedProxy) + ListTile( + leading: Icon( + Icons.warning_amber_outlined, + color: Theme.of(context).colorScheme.error, + ), + title: const Text('Unknown proxy'), + subtitle: const Text('This proxy profile no longer exists'), + trailing: TextButton( + onPressed: () => + Navigator.pop(context, const _ProxyPickerCleared()), + child: const Text('Clear'), + ), + ), + for (final option in options) + RadioListTile( + value: option.id, + title: Text(option.title), + subtitle: Text(option.subtitle), + secondary: const Icon(Icons.route_outlined), + ), + const SizedBox(height: 12), + ], + ), + ), + ); + } +} diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart index 889f706b..57769e3b 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart @@ -31,8 +31,8 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/co import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_title.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/container_icons.dart'; -import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; -import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart'; +import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/ensure_proxy_started.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; class ContainerListScreen extends HookConsumerWidget { @@ -49,25 +49,8 @@ class ContainerListScreen extends HookConsumerWidget { .read(selectedContainerProvider.notifier) .setContainerId(container.id); - if (context.mounted && result == SetContainerResult.successHasProxy) { - final shouldStartProxy = await ref - .read(startProxyControllerProvider.notifier) - .shouldPromptProxyStart(); - - if (!context.mounted || !shouldStartProxy) { - return; - } - - final dialogResult = await showDialog( - context: context, - builder: (context) { - return const TorDialog(); - }, - ); - - if (dialogResult == true) { - await ref.read(startProxyControllerProvider.notifier).startProxy(); - } + if (context.mounted && result == SetContainerResult.success) { + await ensureProxyStartedForContainer(context, ref, container); } } @@ -202,6 +185,7 @@ class _ContainerCard extends HookConsumerWidget { final containerColor = container.color; final tabCount = container.tabCount ?? 0; final palette = ContainerColors.palette(context, containerColor); + final proxyOptions = ref.watch(proxyConnectionOptionsProvider); return AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -266,10 +250,13 @@ class _ContainerCard extends HookConsumerWidget { icon: Icons.cookie_outlined, label: 'Isolated', ), - if (container.metadata.useProxy) - const _ContainerInfoChip( + if (container.metadata.proxyConnectionId != null) + _ContainerInfoChip( icon: Icons.route_outlined, - label: 'Proxy', + label: proxyConnectionTitle( + proxyOptions, + container.metadata.proxyConnectionId!, + ), ), if (container.metadata.clearDataOnExit) const _ContainerInfoChip( diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_selection.dart b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_selection.dart index 530f9f6e..d41c7e39 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_selection.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_selection.dart @@ -34,6 +34,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/co import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_title.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/container_icons.dart'; +import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; class ContainerSelectionScreen extends HookConsumerWidget { @@ -221,7 +222,7 @@ class _UnassignedSelectionCard extends StatelessWidget { } } -class _SelectionContainerCard extends StatelessWidget { +class _SelectionContainerCard extends ConsumerWidget { const _SelectionContainerCard({ required this.container, required this.isSelected, @@ -233,13 +234,15 @@ class _SelectionContainerCard extends StatelessWidget { final VoidCallback onTap; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final containerColor = container.color; final tabCount = container.tabCount ?? 0; final palette = ContainerColors.palette(context, containerColor); + final proxyOptions = ref.watch(proxyConnectionOptionsProvider); + return AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, @@ -306,10 +309,13 @@ class _SelectionContainerCard extends StatelessWidget { icon: Icons.cookie_outlined, label: 'Isolated', ), - if (container.metadata.useProxy) - const _SelectionInfoChip( + if (container.metadata.proxyConnectionId != null) + _SelectionInfoChip( icon: Icons.route_outlined, - label: 'Proxy', + label: proxyConnectionTitle( + proxyOptions, + container.metadata.proxyConnectionId!, + ), ), if (container.metadata.clearDataOnExit) const _SelectionInfoChip( diff --git a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_field.dart b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_field.dart new file mode 100644 index 00000000..3433fa29 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_field.dart @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +enum SingboxFieldKind { + text, + secret, + integer, + integerList, + port, + boolean, + stringList, +} + +class SingboxProxyFormField { + final String key; + final String label; + final String? helperText; + final String? defaultValue; + final bool required; + final SingboxFieldKind kind; + final int? exactListLength; + final int? minValue; + final int? maxValue; + + const SingboxProxyFormField({ + required this.key, + required this.label, + this.helperText, + this.defaultValue, + this.required = false, + this.kind = SingboxFieldKind.text, + this.exactListLength, + this.minValue, + this.maxValue, + }); + + bool get isSecret => kind == SingboxFieldKind.secret; + bool get isNumber => + kind == SingboxFieldKind.integer || kind == SingboxFieldKind.port; + bool get isIntegerList => kind == SingboxFieldKind.integerList; + bool get isPort => kind == SingboxFieldKind.port; + bool get isBoolean => kind == SingboxFieldKind.boolean; + bool get isStringList => kind == SingboxFieldKind.stringList; +} + +/// Shared parsing of boolean form values. Returns null when the text doesn't +/// look like a boolean (treated as "unset"), so callers can distinguish absent +/// from explicitly-false. +bool? parseFormBool(String value) { + return switch (value.trim().toLowerCase()) { + 'true' || 'yes' || '1' || 'on' => true, + 'false' || 'no' || '0' || 'off' => false, + _ => null, + }; +} + +/// Splits a multi-value form input on newlines or commas, trims, drops blanks. +List splitFormStringList(String value) { + return value + .split(RegExp(r'[\n,]')) + .map((item) => item.trim()) + .where((item) => item.isNotEmpty) + .toList(); +} diff --git a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_spec.dart b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_spec.dart new file mode 100644 index 00000000..132042ec --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_spec.dart @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:convert'; + +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_field.dart'; + +class SingboxProxyFormSpec { + final SingboxProxyProfileType type; + final String outboundType; + final List fields; + + const SingboxProxyFormSpec({ + required this.type, + required this.outboundType, + required this.fields, + }); + + String? validate(Map values) { + for (final field in fields) { + final value = values[field.key]?.trim() ?? ''; + if (field.required && value.isEmpty) { + return '${field.label} is required.'; + } + if (field.isNumber && value.isNotEmpty) { + final parsed = int.tryParse(value); + if (parsed == null || parsed < 1) { + return '${field.label} must be a positive number.'; + } + if (field.isPort && parsed > 65535) { + return '${field.label} must be between 1 and 65535.'; + } + } + if (field.isIntegerList && value.isNotEmpty) { + final items = splitFormStringList(value); + final exactListLength = field.exactListLength; + if (exactListLength != null && items.length != exactListLength) { + return '${field.label} must contain $exactListLength numbers.'; + } + for (final item in items) { + final parsed = int.tryParse(item); + if (parsed == null) { + return '${field.label} must contain only numbers.'; + } + final minValue = field.minValue; + if (minValue != null && parsed < minValue) { + return '${field.label} must contain numbers greater than or equal to $minValue.'; + } + final maxValue = field.maxValue; + if (maxValue != null && parsed > maxValue) { + return '${field.label} must contain numbers less than or equal to $maxValue.'; + } + } + } + if (field.isBoolean && value.isNotEmpty && parseFormBool(value) == null) { + return '${field.label} must be true or false.'; + } + } + + return null; + } + + String toConfigJson(Map values) { + final config = {'type': outboundType}; + for (final field in fields.where((field) => !field.isSecret)) { + final value = values[field.key]?.trim() ?? ''; + if (value.isEmpty) continue; + _setJsonValue(config, field.key, _fieldJsonValue(field, value)); + } + + return const JsonEncoder.withIndent(' ').convert(config); + } + + String? toSecretJson(Map values) { + final secrets = {}; + for (final field in fields.where((field) => field.isSecret)) { + final value = values[field.key]?.trim() ?? ''; + if (value.isEmpty) continue; + _setJsonValue(secrets, field.key, _fieldJsonValue(field, value)); + } + + if (secrets.isEmpty) return null; + return const JsonEncoder.withIndent(' ').convert(secrets); + } + + Map valuesFromJson({ + required String configJson, + String? secretJson, + }) { + final config = _decodeJsonObject(configJson) ?? const {}; + final secrets = secretJson == null + ? const {} + : _decodeJsonObject(secretJson) ?? const {}; + final merged = {...config, ...secrets}; + + return { + for (final field in fields) + field.key: _fieldTextValue( + _jsonValueAt(merged, field.key), + field.defaultValue ?? '', + ), + }; + } +} + +Map? _decodeJsonObject(String rawJson) { + try { + final decoded = jsonDecode(rawJson) as Object?; + if (decoded is Map) return decoded; + } catch (_) { + return null; + } + + return null; +} + +Object _fieldJsonValue(SingboxProxyFormField field, String value) { + if (field.isNumber) return int.parse(value); + if (field.isIntegerList) { + return splitFormStringList(value).map(int.parse).toList(); + } + if (field.isBoolean) return parseFormBool(value)!; + if (field.isStringList) return splitFormStringList(value); + return value; +} + +String _fieldTextValue(Object? value, String fallback) { + if (value == null) return fallback; + if (value is List) return value.map((item) => item.toString()).join('\n'); + return value.toString(); +} + +void _setJsonValue(Map target, String path, Object value) { + final segments = path.split('.'); + var current = target; + for (final segment in segments.take(segments.length - 1)) { + current = + current.putIfAbsent(segment, () => {}) + as Map; + } + current[segments.last] = value; +} + +Object? _jsonValueAt(Map source, String path) { + Object? current = source; + for (final segment in path.split('.')) { + if (current is! Map) return null; + current = current[segment]; + } + return current; +} diff --git a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_specs.dart b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_specs.dart new file mode 100644 index 00000000..bb11cfc2 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_specs.dart @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_field.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_spec.dart'; + +const _serverField = SingboxProxyFormField( + key: 'server', + label: 'Server Address', + required: true, +); +const _serverPortField = SingboxProxyFormField( + key: 'server_port', + label: 'Server Port', + required: true, + kind: SingboxFieldKind.port, +); +const _usernameField = SingboxProxyFormField( + key: 'username', + label: 'Username', + kind: SingboxFieldKind.secret, +); +const _passwordField = SingboxProxyFormField( + key: 'password', + label: 'Password', + required: true, + kind: SingboxFieldKind.secret, +); +const _optionalPasswordField = SingboxProxyFormField( + key: 'password', + label: 'Password', + kind: SingboxFieldKind.secret, +); +const _uuidField = SingboxProxyFormField( + key: 'uuid', + label: 'UUID', + required: true, + kind: SingboxFieldKind.secret, +); +const _tlsFields = [ + SingboxProxyFormField( + key: 'tls.enabled', + label: 'TLS Enabled', + helperText: 'true or false.', + kind: SingboxFieldKind.boolean, + ), + SingboxProxyFormField(key: 'tls.server_name', label: 'TLS Server Name'), + SingboxProxyFormField( + key: 'tls.insecure', + label: 'Allow Invalid TLS Certificates', + helperText: 'true or false.', + kind: SingboxFieldKind.boolean, + ), + SingboxProxyFormField( + key: 'tls.alpn', + label: 'TLS ALPN', + helperText: 'Comma-separated or one value per line.', + kind: SingboxFieldKind.stringList, + ), +]; +const _transportFields = [ + SingboxProxyFormField( + key: 'transport.type', + label: 'Transport Type', + helperText: 'For example ws, http, grpc, or quic.', + ), + SingboxProxyFormField(key: 'transport.path', label: 'Transport Path'), + SingboxProxyFormField( + key: 'transport.service_name', + label: 'gRPC Service Name', + ), +]; +const _multiplexFields = [ + SingboxProxyFormField( + key: 'multiplex.enabled', + label: 'Multiplex Enabled', + helperText: 'true or false.', + kind: SingboxFieldKind.boolean, + ), + SingboxProxyFormField(key: 'multiplex.protocol', label: 'Multiplex Protocol'), + SingboxProxyFormField( + key: 'multiplex.max_connections', + label: 'Multiplex Max Connections', + kind: SingboxFieldKind.integer, + ), +]; +const _dialFields = [ + SingboxProxyFormField(key: 'detour', label: 'Dial Detour'), + SingboxProxyFormField(key: 'bind_interface', label: 'Bind Interface'), + SingboxProxyFormField( + key: 'routing_mark', + label: 'Routing Mark', + kind: SingboxFieldKind.integer, + ), + SingboxProxyFormField( + key: 'domain_strategy', + label: 'Domain Strategy', + helperText: 'For example prefer_ipv4 or prefer_ipv6.', + ), + SingboxProxyFormField( + key: 'connect_timeout', + label: 'Connect Timeout', + helperText: 'For example 5s.', + ), +]; +const _v2rayAdvancedFields = [ + ..._tlsFields, + ..._transportFields, + ..._multiplexFields, + ..._dialFields, +]; +const _commonAdvancedFields = [..._multiplexFields, ..._dialFields]; + +const singboxProxyFormSpecs = { + SingboxProxyProfileType.socks: SingboxProxyFormSpec( + type: SingboxProxyProfileType.socks, + outboundType: 'socks', + fields: [ + _serverField, + _serverPortField, + SingboxProxyFormField( + key: 'version', + label: 'SOCKS Version', + defaultValue: '5', + kind: SingboxFieldKind.integer, + ), + _usernameField, + _optionalPasswordField, + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.http: SingboxProxyFormSpec( + type: SingboxProxyProfileType.http, + outboundType: 'http', + fields: [ + _serverField, + _serverPortField, + _usernameField, + _optionalPasswordField, + ..._tlsFields, + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.shadowsocks: SingboxProxyFormSpec( + type: SingboxProxyProfileType.shadowsocks, + outboundType: 'shadowsocks', + fields: [ + _serverField, + _serverPortField, + SingboxProxyFormField( + key: 'method', + label: 'Method', + defaultValue: '2022-blake3-aes-128-gcm', + required: true, + ), + _passwordField, + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.vmess: SingboxProxyFormSpec( + type: SingboxProxyProfileType.vmess, + outboundType: 'vmess', + fields: [ + _serverField, + _serverPortField, + _uuidField, + SingboxProxyFormField( + key: 'security', + label: 'Security', + defaultValue: 'auto', + ), + SingboxProxyFormField( + key: 'alter_id', + label: 'Alter ID', + kind: SingboxFieldKind.integer, + ), + ..._v2rayAdvancedFields, + ], + ), + SingboxProxyProfileType.vless: SingboxProxyFormSpec( + type: SingboxProxyProfileType.vless, + outboundType: 'vless', + fields: [ + _serverField, + _serverPortField, + _uuidField, + SingboxProxyFormField(key: 'flow', label: 'Flow'), + ..._v2rayAdvancedFields, + ], + ), + SingboxProxyProfileType.trojan: SingboxProxyFormSpec( + type: SingboxProxyProfileType.trojan, + outboundType: 'trojan', + fields: [ + _serverField, + _serverPortField, + _passwordField, + ..._v2rayAdvancedFields, + ], + ), + SingboxProxyProfileType.naive: SingboxProxyFormSpec( + type: SingboxProxyProfileType.naive, + outboundType: 'naive', + fields: [ + _serverField, + _serverPortField, + _usernameField, + _passwordField, + ..._tlsFields, + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.hysteria: SingboxProxyFormSpec( + type: SingboxProxyProfileType.hysteria, + outboundType: 'hysteria', + fields: [ + _serverField, + _serverPortField, + SingboxProxyFormField( + key: 'auth_str', + label: 'Auth String', + kind: SingboxFieldKind.secret, + ), + SingboxProxyFormField(key: 'up', label: 'Upload Bandwidth'), + SingboxProxyFormField(key: 'down', label: 'Download Bandwidth'), + SingboxProxyFormField( + key: 'obfs', + label: 'Obfuscation', + kind: SingboxFieldKind.secret, + ), + SingboxProxyFormField( + key: 'recv_window_conn', + label: 'Receive Window Conn', + kind: SingboxFieldKind.integer, + ), + SingboxProxyFormField( + key: 'recv_window', + label: 'Receive Window', + kind: SingboxFieldKind.integer, + ), + SingboxProxyFormField( + key: 'disable_mtu_discovery', + label: 'Disable MTU Discovery', + helperText: 'true or false.', + kind: SingboxFieldKind.boolean, + ), + ..._tlsFields, + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.hysteria2: SingboxProxyFormSpec( + type: SingboxProxyProfileType.hysteria2, + outboundType: 'hysteria2', + fields: [ + _serverField, + _serverPortField, + _passwordField, + SingboxProxyFormField( + key: 'up_mbps', + label: 'Upload Mbps', + kind: SingboxFieldKind.integer, + ), + SingboxProxyFormField( + key: 'down_mbps', + label: 'Download Mbps', + kind: SingboxFieldKind.integer, + ), + SingboxProxyFormField(key: 'obfs.type', label: 'Obfuscation Type'), + SingboxProxyFormField( + key: 'obfs.password', + label: 'Obfuscation Password', + kind: SingboxFieldKind.secret, + ), + ..._tlsFields, + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.tuic: SingboxProxyFormSpec( + type: SingboxProxyProfileType.tuic, + outboundType: 'tuic', + fields: [ + _serverField, + _serverPortField, + _uuidField, + _passwordField, + SingboxProxyFormField( + key: 'congestion_control', + label: 'Congestion Control', + defaultValue: 'cubic', + ), + SingboxProxyFormField( + key: 'udp_relay_mode', + label: 'UDP Relay Mode', + defaultValue: 'native', + ), + SingboxProxyFormField( + key: 'zero_rtt_handshake', + label: 'Zero RTT Handshake', + helperText: 'true or false.', + kind: SingboxFieldKind.boolean, + ), + ..._tlsFields, + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.ssh: SingboxProxyFormSpec( + type: SingboxProxyProfileType.ssh, + outboundType: 'ssh', + fields: [ + _serverField, + _serverPortField, + SingboxProxyFormField(key: 'user', label: 'User', required: true), + _optionalPasswordField, + SingboxProxyFormField( + key: 'private_key', + label: 'Private Key', + kind: SingboxFieldKind.secret, + ), + SingboxProxyFormField( + key: 'private_key_passphrase', + label: 'Private Key Passphrase', + kind: SingboxFieldKind.secret, + ), + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.wireguard: SingboxProxyFormSpec( + type: SingboxProxyProfileType.wireguard, + outboundType: 'wireguard', + fields: [ + _serverField, + _serverPortField, + SingboxProxyFormField( + key: 'local_address', + label: 'Local Address', + helperText: 'One CIDR per line, for example 10.0.0.2/32.', + required: true, + kind: SingboxFieldKind.stringList, + ), + SingboxProxyFormField( + key: 'peer_public_key', + label: 'Peer Public Key', + required: true, + ), + SingboxProxyFormField( + key: 'private_key', + label: 'Private Key', + helperText: 'Stored in secure storage, not profile JSON.', + required: true, + kind: SingboxFieldKind.secret, + ), + SingboxProxyFormField( + key: 'pre_shared_key', + label: 'Pre-shared Key', + helperText: 'Optional. Stored in secure storage.', + kind: SingboxFieldKind.secret, + ), + SingboxProxyFormField( + key: 'mtu', + label: 'MTU', + helperText: 'Optional. sing-box uses 1408 by default.', + defaultValue: '1408', + kind: SingboxFieldKind.integer, + ), + SingboxProxyFormField( + key: 'reserved', + label: 'Reserved Bytes', + helperText: 'Optional. Three comma-separated numbers, e.g. 0,0,0.', + kind: SingboxFieldKind.integerList, + exactListLength: 3, + minValue: 0, + maxValue: 255, + ), + ], + ), + SingboxProxyProfileType.shadowTls: SingboxProxyFormSpec( + type: SingboxProxyProfileType.shadowTls, + outboundType: 'shadowtls', + fields: [ + _serverField, + _serverPortField, + SingboxProxyFormField( + key: 'version', + label: 'Version', + defaultValue: '3', + kind: SingboxFieldKind.integer, + ), + _passwordField, + ..._tlsFields, + ..._commonAdvancedFields, + ], + ), + SingboxProxyProfileType.anyTls: SingboxProxyFormSpec( + type: SingboxProxyProfileType.anyTls, + outboundType: 'anytls', + fields: [_serverField, _serverPortField, _passwordField, ..._dialFields], + ), +}; diff --git a/apps/weblibre/lib/features/proxy/data/models/proxy_log_message.dart b/apps/weblibre/lib/features/proxy/data/models/proxy_log_message.dart new file mode 100644 index 00000000..f7cd1032 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/models/proxy_log_message.dart @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_tor/flutter_tor.dart'; + +enum ProxyLogSource { singBox, tor } + +class ProxyLogMessage with FastEquatable { + final ProxyLogSource source; + final String level; + final String message; + final int timestamp; + final String? profileId; + + ProxyLogMessage({ + required this.source, + required this.level, + required this.message, + required this.timestamp, + this.profileId, + }); + + factory ProxyLogMessage.fromSingbox(SingboxProxyLogMessage message) { + return ProxyLogMessage( + source: ProxyLogSource.singBox, + level: message.level, + message: message.message, + timestamp: message.timestamp, + profileId: message.profileId, + ); + } + + factory ProxyLogMessage.fromTor(TorLogMessage message) { + return ProxyLogMessage( + source: ProxyLogSource.tor, + level: _torSeverityToLevel(message.severity), + message: message.message, + timestamp: message.timestamp, + ); + } + + @override + List get hashParameters => [ + source, + level, + message, + timestamp, + profileId, + ]; +} + +String _torSeverityToLevel(String severity) { + return switch (severity.toUpperCase()) { + 'ERR' => 'error', + 'WARN' => 'warn', + 'DEBUG' => 'debug', + 'INFO' || 'NOTICE' => 'info', + _ => severity.toLowerCase(), + }; +} diff --git a/apps/weblibre/lib/features/proxy/data/models/proxy_profile_seed.dart b/apps/weblibre/lib/features/proxy/data/models/proxy_profile_seed.dart new file mode 100644 index 00000000..e51ed128 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/models/proxy_profile_seed.dart @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; + +/// Pre-filled state handed to the create-mode editor when the user reached it +/// through a guided method (file import, clipboard, QR, etc.). +/// +/// Carries structured [values] for [SingboxProxyFormSpec]-driven types. +/// [dnsOverrideJson] is applied to the editor's DNS override section when +/// present. +class ProxyProfileSeed with FastEquatable { + final SingboxProxyProfileType type; + final String? name; + final Map values; + final String? dnsOverrideJson; + + ProxyProfileSeed({ + required this.type, + this.name, + this.values = const {}, + this.dnsOverrideJson, + }); + + @override + List get hashParameters => [type, name, values, dnsOverrideJson]; +} diff --git a/apps/weblibre/lib/features/proxy/data/models/proxy_share.dart b/apps/weblibre/lib/features/proxy/data/models/proxy_share.dart new file mode 100644 index 00000000..59045aec --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/models/proxy_share.dart @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:convert'; + +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; + +/// Custom share-link scheme for round-tripping a WebLibre proxy profile +/// (including any secret config) between WebLibre installs. NOT cross-app +/// compatible — for that use the standard ss://, vless://, etc. URIs imported +/// from the editor (and exposed by future per-protocol exporters). +const weblibreProxyShareScheme = 'weblibre-proxy'; + +class ProxyShareEnvelope with FastEquatable { + /// Schema version — bumped if the wrapped JSON shape ever changes. + /// v1: name + type + config + secret. + /// v2: also carries dnsOverrideJson so per-profile DNS settings survive + /// share/import. Older v1 payloads stay readable (treated as no + /// override). + static const int currentVersion = 2; + + final String name; + final SingboxProxyProfileType type; + final String configJson; + final String? secretJson; + final String? dnsOverrideJson; + + ProxyShareEnvelope({ + required this.name, + required this.type, + required this.configJson, + this.secretJson, + this.dnsOverrideJson, + }); + + @override + List get hashParameters => [ + name, + type, + configJson, + secretJson, + dnsOverrideJson, + ]; +} + +/// Encodes the profile as `weblibre-proxy://`. The JSON body +/// is compact (no whitespace) to keep the URL short for QR rendering once +/// added. +String encodeProxyShareUri(ProxyShareEnvelope envelope) { + final body = { + 'v': ProxyShareEnvelope.currentVersion, + 'name': envelope.name, + 'type': envelope.type.name, + 'config': jsonDecode(envelope.configJson), + if (envelope.secretJson != null && envelope.secretJson!.isNotEmpty) + 'secret': jsonDecode(envelope.secretJson!), + if (envelope.dnsOverrideJson != null && + envelope.dnsOverrideJson!.isNotEmpty) + 'dnsOverride': jsonDecode(envelope.dnsOverrideJson!), + }; + final encoded = base64UrlEncode(utf8.encode(jsonEncode(body))); + return '$weblibreProxyShareScheme://$encoded'; +} + +/// Decodes a `weblibre-proxy://...` share URI. Throws [FormatException] on +/// malformed input or version mismatch. +const _shareUriPrefix = '$weblibreProxyShareScheme://'; + +ProxyShareEnvelope decodeProxyShareUri(String rawUri) { + final trimmed = rawUri.trim(); + if (!trimmed.startsWith(_shareUriPrefix)) { + throw const FormatException( + 'Not a WebLibre proxy share URI (expected scheme $weblibreProxyShareScheme).', + ); + } + final payload = trimmed.substring(_shareUriPrefix.length); + final List bytes; + try { + bytes = base64Url.decode(base64Url.normalize(payload)); + } on FormatException { + throw const FormatException('Share URI payload is not valid base64url.'); + } + + final dynamic decoded; + try { + decoded = jsonDecode(utf8.decode(bytes)); + } on FormatException { + throw const FormatException('Share URI payload is not valid JSON.'); + } + if (decoded is! Map) { + throw const FormatException('Share URI payload must be a JSON object.'); + } + + final version = decoded['v']; + if (version is! int || + version < 1 || + version > ProxyShareEnvelope.currentVersion) { + throw FormatException('Unsupported share URI version: $version.'); + } + + final name = decoded['name']; + final typeName = decoded['type']; + final config = decoded['config']; + final secret = decoded['secret']; + final dnsOverride = decoded['dnsOverride']; + + if (name is! String || typeName is! String || config == null) { + throw const FormatException('Share URI is missing required fields.'); + } + final type = SingboxProxyProfileType.values + .where((value) => value.name == typeName) + .firstOrNull; + if (type == null) { + throw FormatException('Unknown profile type: $typeName.'); + } + + return ProxyShareEnvelope( + name: name, + type: type, + configJson: jsonEncode(config), + secretJson: secret == null ? null : jsonEncode(secret), + dnsOverrideJson: dnsOverride == null ? null : jsonEncode(dnsOverride), + ); +} diff --git a/apps/weblibre/lib/features/proxy/data/models/singbox_proxy_profile.dart b/apps/weblibre/lib/features/proxy/data/models/singbox_proxy_profile.dart new file mode 100644 index 00000000..5185a3ee --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/models/singbox_proxy_profile.dart @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; + +extension ProxyProfileExt on ProxyProfile { + ProxyConnectionId get proxyConnection => SingboxProxyConnectionId(id); + + String get proxyConnectionId => proxyConnection.encode(); + + SingboxProxyProfile toRuntimeProfile({String? secretJson}) { + return SingboxProxyProfile( + id: proxyConnectionId, + name: name, + type: type, + configJson: configJson, + secretJson: secretJson, + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/data/models/wireguard_config_import.dart b/apps/weblibre/lib/features/proxy/data/models/wireguard_config_import.dart new file mode 100644 index 00000000..60c6d735 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/models/wireguard_config_import.dart @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:convert'; + +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:weblibre/features/proxy/data/parsers/host_port.dart'; + +class WireguardConfigImport with FastEquatable { + /// Values that seed the shared [SingboxProxyFormSpec]-driven WireGuard form. + final Map values; + + /// Newline-separated DNS servers parsed from the WireGuard `[Interface] DNS` + /// line. Not round-tripped through sing-box outbound JSON because DNS lives + /// in the top-level `dns` block and is surfaced as a per-profile override. + final String dns; + + WireguardConfigImport({required this.values, this.dns = ''}); + + @override + List get hashParameters => [values, dns]; + + factory WireguardConfigImport.fromConfigText(String configText) { + final sections = _parseWireguardConfig(configText); + final interface = sections['interface']; + final peer = sections['peer']; + if (interface == null || peer == null) { + throw const FormatException( + 'WireGuard config must contain [Interface] and [Peer] sections.', + ); + } + + final rawEndpoint = (peer['endpoint'] ?? '').trim(); + final endpoint = rawEndpoint.isEmpty + ? (host: '', port: '') + : parseHostPort( + rawEndpoint, + invalidMessage: 'WireGuard endpoint must be host:port.', + ); + final mtu = interface['mtu']?.trim(); + + return WireguardConfigImport( + values: { + 'server': endpoint.host, + 'server_port': endpoint.port, + 'local_address': _wireguardListValue(interface['address']), + 'private_key': interface['privatekey']?.trim() ?? '', + 'peer_public_key': peer['publickey']?.trim() ?? '', + 'pre_shared_key': peer['presharedkey']?.trim() ?? '', + 'mtu': mtu == null || mtu.isEmpty ? '1408' : mtu, + }, + dns: _wireguardListValue(interface['dns']), + ); + } + + /// First parsed DNS entry as a sing-box-compatible address, or null when + /// the imported config had no `DNS = …` line. Bare IPs become `udp://` + /// (sing-box's plain-UDP scheme); anything already containing `://` is kept + /// verbatim so users can paste `https://…/dns-query` etc. + String? get primaryDnsAddress { + final entries = _splitList(dns); + if (entries.isEmpty) return null; + final first = entries.first; + if (first.contains('://')) return first; + return 'udp://$first'; + } +} + +List _splitList(String value) { + return value + .replaceAll('[', '') + .replaceAll(']', '') + .split(RegExp(r'[\n,]')) + .map((item) => item.trim()) + .where((item) => item.isNotEmpty) + .toList(); +} + +Map> _parseWireguardConfig(String configText) { + final sections = >{}; + String? currentSection; + + for (final rawLine in const LineSplitter().convert(configText)) { + final line = _stripWireguardComment(rawLine).trim(); + if (line.isEmpty) continue; + + if (line.startsWith('[') && line.endsWith(']')) { + currentSection = line.substring(1, line.length - 1).trim().toLowerCase(); + if (currentSection.isEmpty) { + throw const FormatException('WireGuard config contains empty section.'); + } + sections.putIfAbsent(currentSection, () => {}); + continue; + } + + if (currentSection == null) { + throw const FormatException( + 'WireGuard config entries must be inside a section.', + ); + } + + final separatorIndex = line.indexOf('='); + if (separatorIndex < 1) { + throw FormatException('Invalid WireGuard config line: $rawLine'); + } + + final key = line.substring(0, separatorIndex).trim().toLowerCase(); + final value = line.substring(separatorIndex + 1).trim(); + if (key.isEmpty) { + throw FormatException('Invalid WireGuard config line: $rawLine'); + } + sections[currentSection]![key] = value; + } + + return sections; +} + +String _stripWireguardComment(String line) { + final hashIndex = line.indexOf('#'); + final semicolonIndex = line.indexOf(';'); + final indexes = [ + if (hashIndex >= 0) hashIndex, + if (semicolonIndex >= 0) semicolonIndex, + ]; + if (indexes.isEmpty) return line; + indexes.sort(); + return line.substring(0, indexes.first); +} + +String _wireguardListValue(String? value) { + if (value == null) return ''; + return value + .split(',') + .map((item) => item.trim()) + .where((item) => item.isNotEmpty) + .join('\n'); +} diff --git a/apps/weblibre/lib/features/proxy/data/parsers/base64_text.dart b/apps/weblibre/lib/features/proxy/data/parsers/base64_text.dart new file mode 100644 index 00000000..0e4685f3 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/parsers/base64_text.dart @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:convert'; + +/// Decodes URL-safe or standard base64 text, tolerating missing padding. +String decodeBase64Text(String value) { + final normalized = value.replaceAll('-', '+').replaceAll('_', '/'); + final padded = normalized.padRight( + normalized.length + (4 - normalized.length % 4) % 4, + '=', + ); + return utf8.decode(base64Decode(padded)); +} diff --git a/apps/weblibre/lib/features/proxy/data/parsers/host_port.dart b/apps/weblibre/lib/features/proxy/data/parsers/host_port.dart new file mode 100644 index 00000000..e6a7f9ea --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/parsers/host_port.dart @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +typedef HostPort = ({String host, String port}); + +/// Parses a `host:port` endpoint. Accepts bracketed IPv6 syntax +/// (`[2001:db8::1]:443`). Throws [FormatException] on invalid input. +HostPort parseHostPort( + String rawEndpoint, { + String invalidMessage = 'Endpoint must be host:port.', +}) { + final endpoint = rawEndpoint.trim(); + if (endpoint.startsWith('[')) { + final closingIndex = endpoint.indexOf(']'); + if (closingIndex < 0 || closingIndex == endpoint.length - 1) { + throw FormatException(invalidMessage); + } + final remainder = endpoint.substring(closingIndex + 1); + if (!remainder.startsWith(':')) { + throw FormatException(invalidMessage); + } + return ( + host: endpoint.substring(1, closingIndex), + port: remainder.substring(1), + ); + } + + final separatorIndex = endpoint.lastIndexOf(':'); + if (separatorIndex <= 0 || separatorIndex == endpoint.length - 1) { + throw FormatException(invalidMessage); + } + final host = endpoint.substring(0, separatorIndex); + if (host.contains(':')) { + throw const FormatException( + 'IPv6 endpoints must use [address]:port syntax.', + ); + } + return (host: host, port: endpoint.substring(separatorIndex + 1)); +} diff --git a/apps/weblibre/lib/features/proxy/data/parsers/singbox_proxy_uri.dart b/apps/weblibre/lib/features/proxy/data/parsers/singbox_proxy_uri.dart new file mode 100644 index 00000000..0b6c7c6f --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/parsers/singbox_proxy_uri.dart @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:convert'; + +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:weblibre/features/proxy/data/parsers/base64_text.dart'; +import 'package:weblibre/features/proxy/data/parsers/host_port.dart'; + +/// Form-spec-shaped result of parsing a single proxy URI (ss://, vless://, …). +class SingboxProxyUriImport with FastEquatable { + final SingboxProxyProfileType type; + final String? name; + final Map values; + + SingboxProxyUriImport({required this.type, required this.values, this.name}); + + @override + List get hashParameters => [type, name, values]; +} + +/// Dispatches on the URI scheme and delegates to the matching importer. +/// Throws [FormatException] for unknown or malformed schemes. +SingboxProxyUriImport importSingboxProxyUri(String rawUri) { + final trimmedUri = rawUri.trim(); + final schemeEnd = trimmedUri.indexOf('://'); + if (schemeEnd <= 0) { + throw const FormatException('Proxy URI must include a scheme.'); + } + + final scheme = trimmedUri.substring(0, schemeEnd).toLowerCase(); + return switch (scheme) { + 'ss' => _importShadowsocksUri(trimmedUri), + 'socks' || 'socks5' => _importSocksUri(trimmedUri), + 'http' || 'https' => _importHttpUri(trimmedUri), + 'trojan' => _importTrojanUri(trimmedUri), + 'vless' => _importVlessUri(trimmedUri), + 'vmess' => _importVmessUri(trimmedUri), + 'hysteria2' || 'hy2' => _importHysteria2Uri(trimmedUri), + 'tuic' => _importTuicUri(trimmedUri), + _ => throw FormatException('Unsupported proxy URI scheme: $scheme'), + }; +} + +SingboxProxyUriImport _importShadowsocksUri(String rawUri) { + final payload = rawUri.substring('ss://'.length); + final fragmentIndex = payload.indexOf('#'); + final withoutFragment = fragmentIndex >= 0 + ? payload.substring(0, fragmentIndex) + : payload; + final name = fragmentIndex >= 0 + ? Uri.decodeComponent(payload.substring(fragmentIndex + 1)) + : null; + final withoutQuery = withoutFragment.split('?').first; + final decodedPayload = withoutQuery.contains('@') + ? withoutQuery + : decodeBase64Text(withoutQuery); + final atIndex = decodedPayload.lastIndexOf('@'); + if (atIndex <= 0 || atIndex == decodedPayload.length - 1) { + throw const FormatException('Shadowsocks URI must include credentials.'); + } + + final rawCredentials = decodedPayload.substring(0, atIndex); + final credentials = rawCredentials.contains(':') + ? rawCredentials + : decodeBase64Text(rawCredentials); + final credentialSeparator = credentials.indexOf(':'); + if (credentialSeparator <= 0) { + throw const FormatException( + 'Shadowsocks URI credentials must be method:password.', + ); + } + final endpoint = parseHostPort( + decodedPayload.substring(atIndex + 1), + invalidMessage: 'Proxy URI endpoint must be host:port.', + ); + + return SingboxProxyUriImport( + type: SingboxProxyProfileType.shadowsocks, + name: _nonEmptyName(name), + values: { + 'server': endpoint.host, + 'server_port': endpoint.port, + 'method': Uri.decodeComponent( + credentials.substring(0, credentialSeparator), + ), + 'password': Uri.decodeComponent( + credentials.substring(credentialSeparator + 1), + ), + }, + ); +} + +SingboxProxyUriImport _importTrojanUri(String rawUri) { + final uri = Uri.parse(rawUri); + final endpoint = _uriEndpoint(uri, 'Trojan'); + + return SingboxProxyUriImport( + type: SingboxProxyProfileType.trojan, + name: _uriName(uri), + values: { + 'server': endpoint.host, + 'server_port': endpoint.port, + 'password': Uri.decodeComponent(uri.userInfo), + 'tls.enabled': 'true', + ..._tlsImportValues(uri), + }, + ); +} + +SingboxProxyUriImport _importVlessUri(String rawUri) { + final uri = Uri.parse(rawUri); + final endpoint = _uriEndpoint(uri, 'VLESS'); + + return SingboxProxyUriImport( + type: SingboxProxyProfileType.vless, + name: _uriName(uri), + values: { + 'server': endpoint.host, + 'server_port': endpoint.port, + 'uuid': Uri.decodeComponent(uri.userInfo), + 'flow': uri.queryParameters['flow'] ?? '', + ..._tlsImportValues(uri), + }, + ); +} + +SingboxProxyUriImport _importSocksUri(String rawUri) { + final uri = Uri.parse(rawUri); + final endpoint = _uriHostPort(uri, 'SOCKS'); + final userInfo = _splitUserInfo(uri.userInfo); + + return SingboxProxyUriImport( + type: SingboxProxyProfileType.socks, + name: _uriName(uri), + values: { + 'server': endpoint.host, + 'server_port': endpoint.port, + 'version': '5', + 'username': userInfo.username, + 'password': userInfo.password, + }, + ); +} + +SingboxProxyUriImport _importHttpUri(String rawUri) { + final uri = Uri.parse(rawUri); + final endpoint = _uriHostPort(uri, 'HTTP'); + final userInfo = _splitUserInfo(uri.userInfo); + + return SingboxProxyUriImport( + type: SingboxProxyProfileType.http, + name: _uriName(uri), + values: { + 'server': endpoint.host, + 'server_port': endpoint.port, + 'username': userInfo.username, + 'password': userInfo.password, + if (uri.scheme.toLowerCase() == 'https') 'tls.enabled': 'true', + ..._tlsImportValues(uri), + }, + ); +} + +SingboxProxyUriImport _importHysteria2Uri(String rawUri) { + final uri = Uri.parse(rawUri); + final endpoint = _uriEndpoint(uri, 'Hysteria2'); + final query = uri.queryParameters; + + return SingboxProxyUriImport( + type: SingboxProxyProfileType.hysteria2, + name: _uriName(uri), + values: { + 'server': endpoint.host, + 'server_port': endpoint.port, + 'password': Uri.decodeComponent(uri.userInfo), + 'obfs.type': query['obfs'] ?? '', + 'obfs.password': query['obfs-password'] ?? query['obfs_password'] ?? '', + 'tls.enabled': 'true', + ..._tlsImportValues(uri), + }, + ); +} + +SingboxProxyUriImport _importTuicUri(String rawUri) { + final uri = Uri.parse(rawUri); + final endpoint = _uriEndpoint(uri, 'TUIC'); + final userInfo = _splitUserInfo(uri.userInfo); + final query = uri.queryParameters; + + return SingboxProxyUriImport( + type: SingboxProxyProfileType.tuic, + name: _uriName(uri), + values: { + 'server': endpoint.host, + 'server_port': endpoint.port, + 'uuid': userInfo.username, + 'password': userInfo.password, + 'congestion_control': + query['congestion_control'] ?? query['congestion'] ?? 'cubic', + 'udp_relay_mode': query['udp_relay_mode'] ?? 'native', + 'tls.enabled': 'true', + ..._tlsImportValues(uri), + }, + ); +} + +SingboxProxyUriImport _importVmessUri(String rawUri) { + final payload = rawUri.substring('vmess://'.length).split('#').first.trim(); + final decoded = jsonDecode(decodeBase64Text(payload)) as Object?; + if (decoded is! Map) { + throw const FormatException('VMess URI payload must be a JSON object.'); + } + + final server = _stringValue(decoded['add'], ''); + final port = _stringValue(decoded['port'], ''); + final uuid = _stringValue(decoded['id'], ''); + if (server.isEmpty || port.isEmpty || uuid.isEmpty) { + throw const FormatException('VMess URI must include add, port, and id.'); + } + + return SingboxProxyUriImport( + type: SingboxProxyProfileType.vmess, + name: _nonEmptyName(_stringValue(decoded['ps'], '')), + values: { + 'server': server, + 'server_port': port, + 'uuid': uuid, + 'security': _stringValue(decoded['scy'], 'auto'), + 'alter_id': _stringValue(decoded['aid'], ''), + 'tls.enabled': _stringValue(decoded['tls'], '') == 'tls' ? 'true' : '', + 'tls.server_name': _stringValue(decoded['sni'], ''), + 'transport.type': _stringValue(decoded['net'], ''), + 'transport.path': _stringValue(decoded['path'], ''), + }, + ); +} + +HostPort _uriEndpoint(Uri uri, String label) { + final endpoint = _uriHostPort(uri, label); + if (uri.userInfo.isEmpty) { + throw FormatException('$label URI must include credentials.'); + } + return endpoint; +} + +HostPort _uriHostPort(Uri uri, String label) { + final hasExplicitOrDefaultPort = uri.hasPort || uri.port > 0; + if (uri.host.isEmpty || !hasExplicitOrDefaultPort) { + throw FormatException('$label URI must include host and port.'); + } + return (host: uri.host, port: uri.port.toString()); +} + +String? _uriName(Uri uri) { + if (!uri.hasFragment) return null; + return _nonEmptyName(Uri.decodeComponent(uri.fragment)); +} + +String? _nonEmptyName(String? name) { + final trimmed = name?.trim(); + if (trimmed == null || trimmed.isEmpty) return null; + return trimmed; +} + +({String username, String password}) _splitUserInfo(String userInfo) { + if (userInfo.isEmpty) return (username: '', password: ''); + final separatorIndex = userInfo.indexOf(':'); + if (separatorIndex < 0) { + return (username: Uri.decodeComponent(userInfo), password: ''); + } + + return ( + username: Uri.decodeComponent(userInfo.substring(0, separatorIndex)), + password: Uri.decodeComponent(userInfo.substring(separatorIndex + 1)), + ); +} + +Map _tlsImportValues(Uri uri) { + final query = uri.queryParameters; + final values = {}; + final security = query['security'] ?? query['tls']; + if (security == 'tls' || security == '1' || security == 'true') { + values['tls.enabled'] = 'true'; + } + final serverName = query['sni'] ?? query['peer'] ?? query['server_name']; + if (serverName != null && serverName.isNotEmpty) { + values['tls.server_name'] = serverName; + } + final insecure = + query['allowInsecure'] ?? query['allow_insecure'] ?? query['insecure']; + if (insecure != null && insecure.isNotEmpty) { + values['tls.insecure'] = insecure; + } + final alpn = query['alpn']; + if (alpn != null && alpn.isNotEmpty) values['tls.alpn'] = alpn; + return values; +} + +String _stringValue(Object? value, String fallback) { + if (value == null) return fallback; + return value.toString(); +} diff --git a/apps/weblibre/lib/features/proxy/data/proxy_connection.dart b/apps/weblibre/lib/features/proxy/data/proxy_connection.dart new file mode 100644 index 00000000..fdbd33d1 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/data/proxy_connection.dart @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +sealed class ProxyConnectionId { + const ProxyConnectionId(); + + String encode(); + + static ProxyConnectionId? decode(String? raw) { + if (raw == null) return null; + if (raw == TorProxyConnectionId.encoded) { + return const TorProxyConnectionId(); + } + + if (raw.startsWith(SingboxProxyConnectionId._prefix)) { + final profileId = raw.substring(SingboxProxyConnectionId._prefix.length); + if (profileId.isEmpty) return null; + return SingboxProxyConnectionId(profileId); + } + + return null; + } +} + +final class TorProxyConnectionId extends ProxyConnectionId { + static const String encoded = 'tor'; + + const TorProxyConnectionId(); + + @override + String encode() => encoded; + + @override + bool operator ==(Object other) => other is TorProxyConnectionId; + + @override + int get hashCode => encoded.hashCode; +} + +final class SingboxProxyConnectionId extends ProxyConnectionId { + static const String _prefix = 'singbox:'; + + final String profileId; + + const SingboxProxyConnectionId(this.profileId); + + @override + String encode() => '$_prefix$profileId'; + + @override + bool operator ==(Object other) { + return other is SingboxProxyConnectionId && other.profileId == profileId; + } + + @override + int get hashCode => Object.hash(_prefix, profileId); +} diff --git a/apps/weblibre/lib/features/proxy/domain/extensions/singbox_proxy_profile_type_x.dart b/apps/weblibre/lib/features/proxy/domain/extensions/singbox_proxy_profile_type_x.dart new file mode 100644 index 00000000..4e13d1e6 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/extensions/singbox_proxy_profile_type_x.dart @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; + +extension SingboxProxyProfileTypeExt on SingboxProxyProfileType { + /// Long human label for menus, dialogs, subtitles. + String get label => switch (this) { + SingboxProxyProfileType.socks => 'SOCKS', + SingboxProxyProfileType.http => 'HTTP', + SingboxProxyProfileType.shadowsocks => 'Shadowsocks', + SingboxProxyProfileType.vmess => 'VMess', + SingboxProxyProfileType.vless => 'VLESS', + SingboxProxyProfileType.trojan => 'Trojan', + SingboxProxyProfileType.naive => 'Naive', + SingboxProxyProfileType.hysteria => 'Hysteria', + SingboxProxyProfileType.hysteria2 => 'Hysteria2', + SingboxProxyProfileType.tuic => 'TUIC', + SingboxProxyProfileType.ssh => 'SSH', + SingboxProxyProfileType.wireguard => 'WireGuard', + SingboxProxyProfileType.shadowTls => 'ShadowTLS', + SingboxProxyProfileType.anyTls => 'AnyTLS', + SingboxProxyProfileType.customOutbound => 'Custom Outbound', + }; + + /// Short 2-5 char protocol abbreviation for the profile-list badge. + String get badge => switch (this) { + SingboxProxyProfileType.socks => 'SOCKS', + SingboxProxyProfileType.http => 'HTTP', + SingboxProxyProfileType.shadowsocks => 'SS', + SingboxProxyProfileType.vmess => 'VMESS', + SingboxProxyProfileType.vless => 'VLESS', + SingboxProxyProfileType.trojan => 'TRJ', + SingboxProxyProfileType.naive => 'NAIVE', + SingboxProxyProfileType.hysteria => 'HY1', + SingboxProxyProfileType.hysteria2 => 'HY2', + SingboxProxyProfileType.tuic => 'TUIC', + SingboxProxyProfileType.ssh => 'SSH', + SingboxProxyProfileType.wireguard => 'WG', + SingboxProxyProfileType.shadowTls => 'STLS', + SingboxProxyProfileType.anyTls => 'ATLS', + SingboxProxyProfileType.customOutbound => 'JSON', + }; +} diff --git a/apps/weblibre/lib/features/proxy/domain/providers/assigned_proxy_profiles.dart b/apps/weblibre/lib/features/proxy/domain/providers/assigned_proxy_profiles.dart new file mode 100644 index 00000000..63477e67 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/providers/assigned_proxy_profiles.dart @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; +import 'package:weblibre/features/proxy/data/models/singbox_proxy_profile.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; +import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart'; + +part 'assigned_proxy_profiles.g.dart'; + +/// Returns sing-box proxy profiles that have at least one routing assignment: +/// global regular-tab routing, private-tab routing, or any container. +@Riverpod(keepAlive: true) +List assignedSingboxProxyProfiles(Ref ref) { + final profiles = + ref.watch(singboxProxyProfilesRepositoryProvider).value ?? const []; + if (profiles.isEmpty) return const []; + + final routing = ref.watch(proxyRoutingSettingsWithDefaultsProvider); + final containers = + ref.watch(watchContainersWithCountProvider).value ?? const []; + + final assignedConnectionIds = { + if (routing.regularTabsMode == ProxyRegularTabRoutingMode.all && + routing.regularTabsProxyConnectionId != null) + routing.regularTabsProxyConnectionId!.encode(), + if (routing.privateTabsProxyConnectionId != null) + routing.privateTabsProxyConnectionId!.encode(), + for (final container in containers) + if (container.metadata.proxyConnectionId != null) + container.metadata.proxyConnectionId!.encode(), + }; + + if (assignedConnectionIds.isEmpty) return const []; + + return [ + for (final profile in profiles) + if (assignedConnectionIds.contains(profile.proxyConnectionId)) profile, + ]; +} diff --git a/apps/weblibre/lib/features/proxy/domain/providers/assigned_proxy_profiles.g.dart b/apps/weblibre/lib/features/proxy/domain/providers/assigned_proxy_profiles.g.dart new file mode 100644 index 00000000..cba8313c --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/providers/assigned_proxy_profiles.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'assigned_proxy_profiles.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Returns sing-box proxy profiles that have at least one routing assignment: +/// global regular-tab routing, private-tab routing, or any container. + +@ProviderFor(assignedSingboxProxyProfiles) +final assignedSingboxProxyProfilesProvider = + AssignedSingboxProxyProfilesProvider._(); + +/// Returns sing-box proxy profiles that have at least one routing assignment: +/// global regular-tab routing, private-tab routing, or any container. + +final class AssignedSingboxProxyProfilesProvider + extends + $FunctionalProvider< + List, + List, + List + > + with $Provider> { + /// Returns sing-box proxy profiles that have at least one routing assignment: + /// global regular-tab routing, private-tab routing, or any container. + AssignedSingboxProxyProfilesProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'assignedSingboxProxyProfilesProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$assignedSingboxProxyProfilesHash(); + + @$internal + @override + $ProviderElement> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + List create(Ref ref) { + return assignedSingboxProxyProfiles(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(List value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$assignedSingboxProxyProfilesHash() => + r'dbc5c429a3b5e4bff902023d363e568c2f955fc4'; diff --git a/apps/weblibre/lib/features/proxy/domain/providers/proxy_connection_options.dart b/apps/weblibre/lib/features/proxy/domain/providers/proxy_connection_options.dart new file mode 100644 index 00000000..529979cc --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/providers/proxy_connection_options.dart @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/proxy/data/models/singbox_proxy_profile.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/extensions/singbox_proxy_profile_type_x.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; + +part 'proxy_connection_options.g.dart'; + +class ProxyConnectionOption with FastEquatable { + final ProxyConnectionId id; + final String title; + final String subtitle; + + ProxyConnectionOption({ + required this.id, + required this.title, + required this.subtitle, + }); + + @override + List get hashParameters => [id, title, subtitle]; +} + +@Riverpod(keepAlive: true) +List proxyConnectionOptions(Ref ref) { + final profilesAsync = ref.watch(singboxProxyProfilesRepositoryProvider); + profilesAsync.whenOrNull( + error: (error, stackTrace) => logger.e( + 'Failed to load sing-box proxy profiles for connection picker', + error: error, + stackTrace: stackTrace, + ), + ); + + final singboxProfiles = profilesAsync.value ?? const []; + + return [ + ProxyConnectionOption( + id: const TorProxyConnectionId(), + title: 'Tor', + subtitle: 'Route through the Tor network', + ), + for (final profile in singboxProfiles) + ProxyConnectionOption( + id: profile.proxyConnection, + title: profile.name, + subtitle: profile.type.label, + ), + ]; +} + +String proxyConnectionTitle( + List options, + ProxyConnectionId proxyConnectionId, +) { + for (final option in options) { + if (option.id == proxyConnectionId) { + return option.title; + } + } + + return 'Unknown proxy'; +} diff --git a/apps/weblibre/lib/features/proxy/domain/providers/proxy_connection_options.g.dart b/apps/weblibre/lib/features/proxy/domain/providers/proxy_connection_options.g.dart new file mode 100644 index 00000000..eecd2ccf --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/providers/proxy_connection_options.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'proxy_connection_options.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(proxyConnectionOptions) +final proxyConnectionOptionsProvider = ProxyConnectionOptionsProvider._(); + +final class ProxyConnectionOptionsProvider + extends + $FunctionalProvider< + List, + List, + List + > + with $Provider> { + ProxyConnectionOptionsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'proxyConnectionOptionsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$proxyConnectionOptionsHash(); + + @$internal + @override + $ProviderElement> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + List create(Ref ref) { + return proxyConnectionOptions(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(List value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$proxyConnectionOptionsHash() => + r'7832968f74acd98189110cf926208e694a20d55a'; diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/container_proxy.dart b/apps/weblibre/lib/features/proxy/domain/repositories/container_proxy.dart new file mode 100644 index 00000000..91b5fdf6 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/container_proxy.dart @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:async'; + +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:synchronized/synchronized.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; + +part 'container_proxy.g.dart'; + +/// Slower-than-default healthcheck timeout for the Tor SOCKS port. Tor's +/// bootstrap can stall when the service is starting up before the proxy +/// service is fully ready; we accept a longer wait here than for other ops. +const _torHealthcheckTimeout = Duration(seconds: 30); + +/// Default healthcheck timeout for ordinary proxy CRUD operations. +const _defaultHealthcheckTimeout = Duration(seconds: 10); + +/// Outer cap on a single healthcheck wait. Exceeded → [TimeoutException]. +const _maxHealthcheckTimeout = Duration(seconds: 15); + +const _healthcheckInitialDelay = Duration(milliseconds: 50); +const _healthcheckMaxDelay = Duration(milliseconds: 500); + +@Riverpod(keepAlive: true) +class ContainerProxyRepository extends _$ContainerProxyRepository { + final _service = GeckoContainerProxyService(); + final _serviceLock = Lock(); + + Future setTorProxyPort(int? port) { + return _runLocked( + healthcheckTimeout: _torHealthcheckTimeout, + body: () { + if (port == null) { + return _service.removeProxy(const TorProxyConnectionId().encode()); + } + + return _service.upsertProxy( + GeckoProxySettings( + id: const TorProxyConnectionId().encode(), + title: 'Tor', + type: 'socks', + host: '127.0.0.1', + port: port, + proxyDNS: true, + doNotProxyLocal: true, + ), + ); + }, + ); + } + + Future upsertProxy(GeckoProxySettings proxy) { + return _runLocked(body: () => _service.upsertProxy(proxy)); + } + + Future removeProxy(String proxyId) { + return _runLocked(body: () => _service.removeProxy(proxyId)); + } + + Future setContainerProxy(String contextId, String proxyId) { + return _runLocked( + body: () => _service.setContainerProxy(contextId, proxyId), + ); + } + + Future clearContainerProxy(String contextId) { + return _runLocked(body: () => _service.clearContainerProxy(contextId)); + } + + Future removeContainerProxyRelation(String contextId, String proxyId) { + return _runLocked( + body: () => _service.removeContainerProxyRelation(contextId, proxyId), + ); + } + + Future setSiteAssignments(List assignements) { + return _runLocked( + body: () => _service.setSiteAssignments( + Map.fromEntries( + assignements.map( + (e) => MapEntry( + e.assignedSite.origin, + e.contextualIdentity ?? 'general', + ), + ), + ), + ), + ); + } + + /// Serialises [body] behind the service lock after the underlying Gecko + /// proxy service reports healthy. Replaces the per-method boilerplate that + /// each public mutator used to repeat. + Future _runLocked({ + required Future Function() body, + Duration healthcheckTimeout = _defaultHealthcheckTimeout, + }) { + return _serviceLock.synchronized(() async { + await _waitHealthcheck().timeout(healthcheckTimeout); + return body(); + }); + } + + /// Polls the Gecko proxy service with exponential backoff (capped) until it + /// reports healthy. The previous implementation polled every 25ms, which + /// burns CPU during the cold-start window before the native plugin is ready. + Future _waitHealthcheck({ + Duration timeout = _maxHealthcheckTimeout, + }) async { + final startTime = DateTime.now(); + var delay = _healthcheckInitialDelay; + + while (!await _service.healthcheck()) { + if (DateTime.now().difference(startTime) > timeout) { + throw TimeoutException('Timed out waiting for proxy service'); + } + + await Future.delayed(delay); + delay = delay * 2; + if (delay > _healthcheckMaxDelay) delay = _healthcheckMaxDelay; + } + } + + @override + void build() { + return; + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/container_proxy.g.dart b/apps/weblibre/lib/features/proxy/domain/repositories/container_proxy.g.dart new file mode 100644 index 00000000..137910ae --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/container_proxy.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'container_proxy.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ContainerProxyRepository) +final containerProxyRepositoryProvider = ContainerProxyRepositoryProvider._(); + +final class ContainerProxyRepositoryProvider + extends $NotifierProvider { + ContainerProxyRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'containerProxyRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$containerProxyRepositoryHash(); + + @$internal + @override + ContainerProxyRepository create() => ContainerProxyRepository(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(void value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$containerProxyRepositoryHash() => + r'08cd408a3ae96c859ed5c9d56d61cf7e6514415f'; + +abstract class _$ContainerProxyRepository extends $Notifier { + void build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + void, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_credentials.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_credentials.dart new file mode 100644 index 00000000..8f42f779 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_credentials.dart @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'singbox_proxy_credentials.g.dart'; + +@Riverpod(keepAlive: true) +class SingboxProxyCredentialsRepository + extends _$SingboxProxyCredentialsRepository { + static const _storage = FlutterSecureStorage(); + + String _secretKey(String profileId) => 'singbox_proxy.secret.$profileId'; + + Future readSecretJson(String profileId) { + return _storage.read(key: _secretKey(profileId)); + } + + Future writeSecretJson(String profileId, String? secretJson) async { + if (secretJson == null || secretJson.trim().isEmpty) { + await deleteSecretJson(profileId); + return; + } + + await _storage.write(key: _secretKey(profileId), value: secretJson); + } + + Future deleteSecretJson(String profileId) { + return _storage.delete(key: _secretKey(profileId)); + } + + @override + void build() { + return; + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_credentials.g.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_credentials.g.dart new file mode 100644 index 00000000..020e7440 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_credentials.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'singbox_proxy_credentials.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(SingboxProxyCredentialsRepository) +final singboxProxyCredentialsRepositoryProvider = + SingboxProxyCredentialsRepositoryProvider._(); + +final class SingboxProxyCredentialsRepositoryProvider + extends $NotifierProvider { + SingboxProxyCredentialsRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'singboxProxyCredentialsRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => + _$singboxProxyCredentialsRepositoryHash(); + + @$internal + @override + SingboxProxyCredentialsRepository create() => + SingboxProxyCredentialsRepository(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(void value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$singboxProxyCredentialsRepositoryHash() => + r'b4e11b001ccccfbf26417963adf6cb81e1b1e69f'; + +abstract class _$SingboxProxyCredentialsRepository extends $Notifier { + void build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + void, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.dart new file mode 100644 index 00000000..d03ed260 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.dart @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_tor/flutter_tor.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_log_message.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; + +part 'singbox_proxy_logs.g.dart'; + +/// Cap to keep memory bounded. ~2KB per line × 2000 = ~4MB worst-case, which +/// is well within budget for a debugging surface. +const int _ringBufferCapacity = 2000; + +/// Snapshot of buffered log entries. Most-recent-last (chronological). +@Riverpod(keepAlive: true) +class SingboxProxyLogs extends _$SingboxProxyLogs { + final _buffer = Queue(); + + StreamSubscription? _singboxSubscription; + StreamSubscription? _torSubscription; + + void _append(ProxyLogMessage message) { + _buffer.add(message); + + while (_buffer.length > _ringBufferCapacity) { + _buffer.removeFirst(); + } + + state = List.unmodifiable(_buffer); + } + + void clear() { + _buffer.clear(); + state = const []; + } + + @override + List build() { + final client = ref.watch(singboxProxyClientProvider); + final torLogs = torLogStream(ref); + + _singboxSubscription = client.logStream.listen( + (message) => _append(ProxyLogMessage.fromSingbox(message)), + ); + _torSubscription = torLogs.listen( + (message) => _append(ProxyLogMessage.fromTor(message)), + ); + + ref.onDispose(() { + unawaited(_singboxSubscription?.cancel()); + unawaited(_torSubscription?.cancel()); + }); + + return List.unmodifiable(_buffer); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.g.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.g.dart new file mode 100644 index 00000000..6b87a8e2 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.g.dart @@ -0,0 +1,67 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'singbox_proxy_logs.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Snapshot of buffered log entries. Most-recent-last (chronological). + +@ProviderFor(SingboxProxyLogs) +final singboxProxyLogsProvider = SingboxProxyLogsProvider._(); + +/// Snapshot of buffered log entries. Most-recent-last (chronological). +final class SingboxProxyLogsProvider + extends $NotifierProvider> { + /// Snapshot of buffered log entries. Most-recent-last (chronological). + SingboxProxyLogsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'singboxProxyLogsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$singboxProxyLogsHash(); + + @$internal + @override + SingboxProxyLogs create() => SingboxProxyLogs(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(List value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$singboxProxyLogsHash() => r'9fa30201ed4c128335142227226d937022f79d47'; + +/// Snapshot of buffered log entries. Most-recent-last (chronological). + +abstract class _$SingboxProxyLogs extends $Notifier> { + List build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, List>, + List, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_profiles.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_profiles.dart new file mode 100644 index 00000000..44fdb25c --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_profiles.dart @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/uuid.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_credentials.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; +import 'package:weblibre/features/user/data/providers.dart'; + +part 'singbox_proxy_profiles.g.dart'; + +@Riverpod(keepAlive: true) +class SingboxProxyProfilesRepository extends _$SingboxProxyProfilesRepository { + Future> fetchProfiles() { + return ref.read(userDatabaseProvider).proxyProfileDao.fetchAll(); + } + + Future findProfile(String id) { + return ref.read(userDatabaseProvider).proxyProfileDao.findById(id); + } + + Future createProfile({ + required String name, + required SingboxProxyProfileType type, + required String configJson, + String? secretJson, + String? dnsOverrideJson, + }) async { + final now = DateTime.now(); + final profile = ProxyProfile( + id: uuid.v4(), + name: name, + type: type, + configJson: configJson, + dnsOverrideJson: dnsOverrideJson, + createdAt: now, + updatedAt: now, + ); + + await ref.read(userDatabaseProvider).proxyProfileDao.upsert(profile); + await ref + .read(singboxProxyCredentialsRepositoryProvider.notifier) + .writeSecretJson(profile.id, secretJson); + return profile; + } + + /// Updates an existing profile, bumping `updatedAt` only when the persisted + /// row actually changed. Pass [secretJson] (possibly null) to replace the + /// stored secret; pass [updateSecret] = false to leave secrets untouched. + Future updateProfile(ProxyProfile profile, {String? secretJson}) async { + final dao = ref.read(userDatabaseProvider).proxyProfileDao; + final existing = await dao.findById(profile.id); + + final contentChanged = + existing == null || + existing.name != profile.name || + existing.type != profile.type || + existing.configJson != profile.configJson || + existing.dnsOverrideJson != profile.dnsOverrideJson; + + if (contentChanged) { + await dao.upsert( + ProxyProfile( + id: profile.id, + name: profile.name, + type: profile.type, + configJson: profile.configJson, + dnsOverrideJson: profile.dnsOverrideJson, + createdAt: existing?.createdAt ?? profile.createdAt, + updatedAt: DateTime.now(), + ), + ); + } + + if (secretJson != null) { + await ref + .read(singboxProxyCredentialsRepositoryProvider.notifier) + .writeSecretJson(profile.id, secretJson); + } + } + + Future deleteProfile(String profileId) async { + await ref.read(userDatabaseProvider).proxyProfileDao.deleteById(profileId); + await ref + .read(singboxProxyCredentialsRepositoryProvider.notifier) + .deleteSecretJson(profileId); + } + + @override + Stream> build() { + return ref.watch(userDatabaseProvider).proxyProfileDao.watch().watch(); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_profiles.g.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_profiles.g.dart new file mode 100644 index 00000000..f2b86f82 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_profiles.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'singbox_proxy_profiles.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(SingboxProxyProfilesRepository) +final singboxProxyProfilesRepositoryProvider = + SingboxProxyProfilesRepositoryProvider._(); + +final class SingboxProxyProfilesRepositoryProvider + extends + $StreamNotifierProvider< + SingboxProxyProfilesRepository, + List + > { + SingboxProxyProfilesRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'singboxProxyProfilesRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$singboxProxyProfilesRepositoryHash(); + + @$internal + @override + SingboxProxyProfilesRepository create() => SingboxProxyProfilesRepository(); +} + +String _$singboxProxyProfilesRepositoryHash() => + r'5250f2aafb7ec621b3b33a53ff5a00874e8a4b12'; + +abstract class _$SingboxProxyProfilesRepository + extends $StreamNotifier> { + Stream> build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_runtime.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_runtime.dart new file mode 100644 index 00000000..618fa02f --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_runtime.dart @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:synchronized/synchronized.dart'; +import 'package:weblibre/features/proxy/data/models/singbox_proxy_profile.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_credentials.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/services/dns_config_resolver.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; +import 'package:weblibre/features/user/data/models/proxy_dns_override.dart'; +import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; + +part 'singbox_proxy_runtime.g.dart'; + +abstract interface class SingboxProxyClient { + Stream get stateStream; + + Stream get logStream; + + Future validateProfile(SingboxProxyProfile profile); + + Future buildConfig( + List profiles, { + SingboxProxyRuntimeOptions? options, + }); + + Future start( + List profiles, { + SingboxProxyRuntimeOptions? options, + }); + + Future stop(List profileIds); + + Future stopAll(); + + Future getState(); + + Future dispose(); +} + +class FlutterSingboxProxyClient implements SingboxProxyClient { + final _plugin = FlutterSingboxProxy(); + + @override + Stream get stateStream => _plugin.stateStream; + + @override + Stream get logStream => _plugin.logStream; + + @override + Future validateProfile(SingboxProxyProfile profile) { + return _plugin.validateProfile(profile); + } + + @override + Future buildConfig( + List profiles, { + SingboxProxyRuntimeOptions? options, + }) { + return _plugin.buildConfig(profiles, options: options); + } + + @override + Future start( + List profiles, { + SingboxProxyRuntimeOptions? options, + }) { + return _plugin.start(profiles, options: options); + } + + @override + Future stop(List profileIds) => _plugin.stop(profileIds); + + @override + Future stopAll() => _plugin.stopAll(); + + @override + Future getState() => _plugin.getState(); + + @override + Future dispose() => _plugin.dispose(); +} + +@Riverpod(keepAlive: true) +SingboxProxyClient singboxProxyClient(Ref ref) { + return FlutterSingboxProxyClient(); +} + +@Riverpod(keepAlive: true) +class SingboxProxyRuntimeRepository extends _$SingboxProxyRuntimeRepository { + final _lock = Lock(); + + SingboxProxyClient get _plugin => ref.read(singboxProxyClientProvider); + + Future _stateSnapshotUnlocked() async { + final currentState = state.asData?.value; + if (currentState != null) return currentState; + + final nextState = await _plugin.getState(); + state = AsyncData(nextState); + return nextState; + } + + Future startProfile( + String profileId, { + SingboxProxyRuntimeOptions? options, + }) async { + return _lock.synchronized(() async { + final currentState = await _stateSnapshotUnlocked(); + final activeProfileIds = _activeProfileIds(currentState); + + return _startProfilesUnlocked( + {...activeProfileIds, profileId}.toList(), + options: options, + ); + }); + } + + Future ensureProxyConnectionAvailable( + SingboxProxyConnectionId connectionId, + ) async { + await _lock.synchronized(() async { + final currentState = await _stateSnapshotUnlocked(); + final isRunning = currentState.endpoints.any( + (endpoint) => endpoint.profileId == connectionId.encode(), + ); + if (isRunning) return; + + final activeProfileIds = _activeProfileIds(currentState); + await _startProfilesUnlocked( + {...activeProfileIds, connectionId.profileId}.toList(), + ); + }); + } + + Set _activeProfileIds(SingboxProxyRuntimeState runtimeState) { + return runtimeState.endpoints + .map((endpoint) => ProxyConnectionId.decode(endpoint.profileId)) + .whereType() + .map((connectionId) => connectionId.profileId) + .toSet(); + } + + Future startProfiles( + List profileIds, { + SingboxProxyRuntimeOptions? options, + }) { + return _lock.synchronized( + () => _startProfilesUnlocked(profileIds, options: options), + ); + } + + Future _startProfilesUnlocked( + List profileIds, { + SingboxProxyRuntimeOptions? options, + }) async { + state = const AsyncLoading(); + + try { + final profiles = await _runtimeProfiles(profileIds); + final resolvedOptions = await _buildRuntimeOptions( + options ?? SingboxProxyRuntimeOptions(), + profileIds: profileIds.toSet(), + ); + + final nextState = await _plugin.start(profiles, options: resolvedOptions); + + state = AsyncData(nextState); + return nextState; + } catch (error, stackTrace) { + state = AsyncError(error, stackTrace); + rethrow; + } + } + + Future _buildRuntimeOptions( + SingboxProxyRuntimeOptions base, { + required Set profileIds, + }) async { + // Don't overwrite a caller-supplied dnsConfig (e.g. tests or one-off + // overrides). + final engineSettings = await ref + .read(engineSettingsRepositoryProvider.notifier) + .fetchSettings(); + final dohUrl = engineSettings.dohProviderUrl; + + if (base.dnsConfig != null) { + return SingboxProxyRuntimeOptions( + preferredBasePort: base.preferredBasePort, + blockUnmatchedTraffic: base.blockUnmatchedTraffic, + dnsConfig: base.dnsConfig, + bootstrapDohUrl: base.bootstrapDohUrl ?? dohUrl, + ); + } + + final profiles = await ref + .read(singboxProxyProfilesRepositoryProvider.notifier) + .fetchProfiles(); + + final overrides = { + for (final profile in profiles) + if (profileIds.contains(profile.id)) + profile.id: _decodeOverride(profile.dnsOverrideJson), + }; + + final dnsConfig = buildDnsConfig( + overridesByProfileId: overrides, + runningProfileIds: profileIds, + browserDohUrl: dohUrl, + ); + + return SingboxProxyRuntimeOptions( + preferredBasePort: base.preferredBasePort, + blockUnmatchedTraffic: base.blockUnmatchedTraffic, + dnsConfig: dnsConfig, + bootstrapDohUrl: dohUrl, + ); + } + + ProxyDnsOverride? _decodeOverride(String? json) { + if (json == null || json.trim().isEmpty) return null; + try { + final decoded = jsonDecode(json); + if (decoded is! Map) return null; + return ProxyDnsOverride.fromJson(decoded); + } catch (_) { + return null; + } + } + + Future stopProfiles(List profileIds) { + return _lock.synchronized(() async { + await _stopProfilesUnlocked(profileIds); + }); + } + + Future deleteProfile(String profileId) { + return _lock.synchronized(() async { + await _stopProfilesUnlocked([profileId]); + await ref + .read(singboxProxyProfilesRepositoryProvider.notifier) + .deleteProfile(profileId); + }); + } + + Future stopAll() { + return _lock.synchronized(() async { + await _plugin.stopAll(); + final nextState = await _plugin.getState(); + state = AsyncData(nextState); + }); + } + + Future validateProfile(ProxyProfile profile) async { + return _plugin.validateProfile(await _runtimeProfile(profile)); + } + + Future _stopProfilesUnlocked(List profileIds) async { + final proxyIds = profileIds + .map((profileId) => SingboxProxyConnectionId(profileId).encode()) + .toList(); + + await _plugin.stop(proxyIds); + final nextState = await _plugin.getState(); + state = AsyncData(nextState); + } + + Future validateProfileDraft( + ProxyProfile profile, { + String? secretJson, + }) { + return _plugin.validateProfile( + profile.toRuntimeProfile(secretJson: secretJson), + ); + } + + Future buildConfig( + List profileIds, { + SingboxProxyRuntimeOptions? options, + }) async { + final resolvedOptions = await _buildRuntimeOptions( + options ?? SingboxProxyRuntimeOptions(), + profileIds: profileIds.toSet(), + ); + return _plugin.buildConfig( + await _runtimeProfiles(profileIds), + options: resolvedOptions, + ); + } + + Future> _runtimeProfiles( + List profileIds, + ) async { + final profiles = await ref + .read(singboxProxyProfilesRepositoryProvider.notifier) + .fetchProfiles(); + final profileMap = {for (final profile in profiles) profile.id: profile}; + + return Future.wait( + profileIds.map((profileId) async { + final profile = profileMap[profileId]; + if (profile == null) { + throw StateError('Unknown sing-box proxy profile: $profileId'); + } + + return _runtimeProfile(profile); + }), + ); + } + + Future _runtimeProfile(ProxyProfile profile) async { + final secretJson = await ref + .read(singboxProxyCredentialsRepositoryProvider.notifier) + .readSecretJson(profile.id); + + return profile.toRuntimeProfile(secretJson: secretJson); + } + + @override + Future build() async { + final plugin = ref.watch(singboxProxyClientProvider); + final stateSubscription = plugin.stateStream.listen((nextState) { + state = AsyncData(nextState); + }); + + ref.onDispose(() async { + await stateSubscription.cancel(); + await plugin.dispose(); + }); + + return plugin.getState(); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_runtime.g.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_runtime.g.dart new file mode 100644 index 00000000..f3c7fcf9 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_runtime.g.dart @@ -0,0 +1,117 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'singbox_proxy_runtime.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(singboxProxyClient) +final singboxProxyClientProvider = SingboxProxyClientProvider._(); + +final class SingboxProxyClientProvider + extends + $FunctionalProvider< + SingboxProxyClient, + SingboxProxyClient, + SingboxProxyClient + > + with $Provider { + SingboxProxyClientProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'singboxProxyClientProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$singboxProxyClientHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + SingboxProxyClient create(Ref ref) { + return singboxProxyClient(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(SingboxProxyClient value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$singboxProxyClientHash() => + r'3e56b277667e92a2d4941fd084dddd0ff25afd52'; + +@ProviderFor(SingboxProxyRuntimeRepository) +final singboxProxyRuntimeRepositoryProvider = + SingboxProxyRuntimeRepositoryProvider._(); + +final class SingboxProxyRuntimeRepositoryProvider + extends + $AsyncNotifierProvider< + SingboxProxyRuntimeRepository, + SingboxProxyRuntimeState + > { + SingboxProxyRuntimeRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'singboxProxyRuntimeRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$singboxProxyRuntimeRepositoryHash(); + + @$internal + @override + SingboxProxyRuntimeRepository create() => SingboxProxyRuntimeRepository(); +} + +String _$singboxProxyRuntimeRepositoryHash() => + r'665908eaff34569f7a19dc5328a067711d1937f2'; + +abstract class _$SingboxProxyRuntimeRepository + extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref< + AsyncValue, + SingboxProxyRuntimeState + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue, + SingboxProxyRuntimeState + >, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/services/browser_dns_leak_guard.dart b/apps/weblibre/lib/features/proxy/domain/services/browser_dns_leak_guard.dart new file mode 100644 index 00000000..22b7bb38 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/browser_dns_leak_guard.dart @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/user/data/models/engine_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; + +part 'browser_dns_leak_guard.g.dart'; + +/// Watches the proxy runtime and **disables** GeckoView's TRR (sets +/// [DohSettingsMode.off]) while at least one profile is running. +/// +/// Why "off" and not "max": TRR resolves URL hostnames over DoH directly via +/// the system network, *before* any SOCKS connection is established — so a +/// DoH lookup leaks the destination outside the proxy even when the data +/// itself goes through it. `max` (TRR-only) keeps that leak. `off` disables +/// TRR so GeckoView uses its native resolver, which — combined with +/// `proxyDNS: true` on our SOCKS proxy settings — sends hostnames through the +/// SOCKS inbound so sing-box can resolve them instead of GeckoView doing a +/// direct DoH lookup first. +/// +/// The previous TRR mode is captured on engage and restored when all +/// profiles stop. +/// +/// This is a side-effect-only provider: it must be `keepAlive: true` and is +/// explicitly listened-to from main.dart so the side effect runs without any +/// widget needing to depend on it. +@Riverpod(keepAlive: true) +class BrowserDnsLeakGuard extends _$BrowserDnsLeakGuard { + DohSettingsMode? _savedMode; + + @override + Future build() async { + final runtime = ref.watch(singboxProxyRuntimeRepositoryProvider); + + // Skip while a start/stop is in flight. `startProfiles` resets the runtime + // state to `AsyncLoading` for the entire restart — `asData` is briefly + // null, which would otherwise look like "no profiles running" and trigger + // a premature DoH restore in the middle of e.g. starting a second profile + // while one is already active, opening a leak window during the transition. + if (runtime.isLoading) return; + + final anyRunning = runtime.asData?.value.endpoints.isNotEmpty ?? false; + if (anyRunning) { + await _enforceOffMode(); + } else if (_savedMode != null) { + await _restoreSavedMode(); + } + } + + Future _enforceOffMode() async { + final engine = ref.read(engineSettingsRepositoryProvider.notifier); + try { + final current = await engine.fetchSettings(); + if (current.dohSettingsMode == DohSettingsMode.off) { + // Already off — don't capture it as the "saved" value, otherwise we + // would restore it back to off on disengage instead of the user's + // real previous choice. + return; + } + _savedMode = current.dohSettingsMode; + await engine.updateSettings( + (current) => current.copyWith.dohSettingsMode(DohSettingsMode.off), + ); + } catch (error, stack) { + logger.e( + 'browser DNS leak guard failed to disable TRR', + error: error, + stackTrace: stack, + ); + } + } + + Future _restoreSavedMode() async { + final saved = _savedMode; + _savedMode = null; + if (saved == null) return; + try { + await ref + .read(engineSettingsRepositoryProvider.notifier) + .updateSettings((current) => current.copyWith.dohSettingsMode(saved)); + } catch (error, stack) { + logger.e( + 'browser DNS leak guard failed to restore DoH mode', + error: error, + stackTrace: stack, + ); + } + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/services/browser_dns_leak_guard.g.dart b/apps/weblibre/lib/features/proxy/domain/services/browser_dns_leak_guard.g.dart new file mode 100644 index 00000000..21b7547f --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/browser_dns_leak_guard.g.dart @@ -0,0 +1,128 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'browser_dns_leak_guard.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Watches the proxy runtime and **disables** GeckoView's TRR (sets +/// [DohSettingsMode.off]) while at least one profile is running. +/// +/// Why "off" and not "max": TRR resolves URL hostnames over DoH directly via +/// the system network, *before* any SOCKS connection is established — so a +/// DoH lookup leaks the destination outside the proxy even when the data +/// itself goes through it. `max` (TRR-only) keeps that leak. `off` disables +/// TRR so GeckoView uses its native resolver, which — combined with +/// `proxyDNS: true` on our SOCKS proxy settings — sends hostnames through the +/// SOCKS inbound so sing-box can resolve them instead of GeckoView doing a +/// direct DoH lookup first. +/// +/// The previous TRR mode is captured on engage and restored when all +/// profiles stop. +/// +/// This is a side-effect-only provider: it must be `keepAlive: true` and is +/// explicitly listened-to from main.dart so the side effect runs without any +/// widget needing to depend on it. + +@ProviderFor(BrowserDnsLeakGuard) +final browserDnsLeakGuardProvider = BrowserDnsLeakGuardProvider._(); + +/// Watches the proxy runtime and **disables** GeckoView's TRR (sets +/// [DohSettingsMode.off]) while at least one profile is running. +/// +/// Why "off" and not "max": TRR resolves URL hostnames over DoH directly via +/// the system network, *before* any SOCKS connection is established — so a +/// DoH lookup leaks the destination outside the proxy even when the data +/// itself goes through it. `max` (TRR-only) keeps that leak. `off` disables +/// TRR so GeckoView uses its native resolver, which — combined with +/// `proxyDNS: true` on our SOCKS proxy settings — sends hostnames through the +/// SOCKS inbound so sing-box can resolve them instead of GeckoView doing a +/// direct DoH lookup first. +/// +/// The previous TRR mode is captured on engage and restored when all +/// profiles stop. +/// +/// This is a side-effect-only provider: it must be `keepAlive: true` and is +/// explicitly listened-to from main.dart so the side effect runs without any +/// widget needing to depend on it. +final class BrowserDnsLeakGuardProvider + extends $AsyncNotifierProvider { + /// Watches the proxy runtime and **disables** GeckoView's TRR (sets + /// [DohSettingsMode.off]) while at least one profile is running. + /// + /// Why "off" and not "max": TRR resolves URL hostnames over DoH directly via + /// the system network, *before* any SOCKS connection is established — so a + /// DoH lookup leaks the destination outside the proxy even when the data + /// itself goes through it. `max` (TRR-only) keeps that leak. `off` disables + /// TRR so GeckoView uses its native resolver, which — combined with + /// `proxyDNS: true` on our SOCKS proxy settings — sends hostnames through the + /// SOCKS inbound so sing-box can resolve them instead of GeckoView doing a + /// direct DoH lookup first. + /// + /// The previous TRR mode is captured on engage and restored when all + /// profiles stop. + /// + /// This is a side-effect-only provider: it must be `keepAlive: true` and is + /// explicitly listened-to from main.dart so the side effect runs without any + /// widget needing to depend on it. + BrowserDnsLeakGuardProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'browserDnsLeakGuardProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$browserDnsLeakGuardHash(); + + @$internal + @override + BrowserDnsLeakGuard create() => BrowserDnsLeakGuard(); +} + +String _$browserDnsLeakGuardHash() => + r'366aaa6ae8c163449dca50147be0451c766ba765'; + +/// Watches the proxy runtime and **disables** GeckoView's TRR (sets +/// [DohSettingsMode.off]) while at least one profile is running. +/// +/// Why "off" and not "max": TRR resolves URL hostnames over DoH directly via +/// the system network, *before* any SOCKS connection is established — so a +/// DoH lookup leaks the destination outside the proxy even when the data +/// itself goes through it. `max` (TRR-only) keeps that leak. `off` disables +/// TRR so GeckoView uses its native resolver, which — combined with +/// `proxyDNS: true` on our SOCKS proxy settings — sends hostnames through the +/// SOCKS inbound so sing-box can resolve them instead of GeckoView doing a +/// direct DoH lookup first. +/// +/// The previous TRR mode is captured on engage and restored when all +/// profiles stop. +/// +/// This is a side-effect-only provider: it must be `keepAlive: true` and is +/// explicitly listened-to from main.dart so the side effect runs without any +/// widget needing to depend on it. + +abstract class _$BrowserDnsLeakGuard extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/services/dns_config_resolver.dart b/apps/weblibre/lib/features/proxy/domain/services/dns_config_resolver.dart new file mode 100644 index 00000000..a84dac9b --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/dns_config_resolver.dart @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/user/data/models/proxy_dns_override.dart'; + +/// Builds a sing-box [SingboxProxyDnsConfig] from automatic browser-DNS +/// mirroring plus per-profile [overridesByProfileId]. +/// +/// Profiles without an override reuse the browser DoH URL, detoured through the +/// profile and scoped to that profile's SOCKS inbound. Profiles with an +/// override use their own resolver instead. +/// +/// Returns null when there is nothing to configure, letting sing-box fall back +/// to its built-in resolver behaviour. +SingboxProxyDnsConfig? buildDnsConfig({ + required Map overridesByProfileId, + required Set runningProfileIds, + required String? browserDohUrl, +}) { + final servers = []; + final hasBrowserDoh = browserDohUrl != null && browserDohUrl.isNotEmpty; + + if (hasBrowserDoh) { + servers.add( + SingboxProxyDnsServerConfig(tag: 'browser-doh', address: browserDohUrl), + ); + + // Mirror the browser DoH URL through each running profile unless that + // profile has an explicit override. Scope by inbound so endpoint bootstrap + // lookups do not route through the not-yet-ready outbound and deadlock. + for (final profileId in runningProfileIds) { + if (overridesByProfileId[profileId] != null) { + continue; + } + + final outboundTag = _outboundTagForProfile(profileId); + final inboundTag = _inboundTagForProfile(profileId); + + servers.add( + SingboxProxyDnsServerConfig( + tag: 'browser-doh-${singboxSanitizeTag(profileId)}', + address: browserDohUrl, + detourTag: outboundTag, + matchInbounds: [inboundTag], + ), + ); + } + } + + overridesByProfileId.forEach((profileId, override) { + if (override == null || !runningProfileIds.contains(profileId)) { + return; + } + + final outboundTag = _outboundTagForProfile(profileId); + final inboundTag = _inboundTagForProfile(profileId); + + final address = override.remoteServerAddress; + if (address == null || address.isEmpty) { + return; + } + + servers.add( + SingboxProxyDnsServerConfig( + tag: 'override-${singboxSanitizeTag(profileId)}', + address: address, + detourTag: outboundTag, + matchInbounds: [inboundTag], + ), + ); + }); + + if (servers.isEmpty) return null; + return SingboxProxyDnsConfig( + servers: servers, + finalServerTag: hasBrowserDoh ? 'browser-doh' : null, + domainStrategy: _domainStrategy(overridesByProfileId).singboxValue, + ); +} + +ProxyDnsDomainStrategy _domainStrategy( + Map overridesByProfileId, +) { + for (final override in overridesByProfileId.values) { + if (override != null) return override.domainStrategy; + } + return ProxyDnsDomainStrategy.preferIpv4; +} + +/// Must mirror Kotlin `SingboxTagFormat.outboundTag(profileId)`. +/// +/// The Kotlin builder receives the *runtime* profile id (which Dart prefixes +/// with `singbox:` via [SingboxProxyConnectionId] in +/// `ProxyProfileX.toRuntimeProfile`), then sanitises it. We must therefore +/// apply the same prefix here so the detour tag we emit references the same +/// outbound the builder actually created. +/// +/// The mirrored Kotlin test lives in `SingboxTagFormatTest.kt` — both must +/// update together if this format ever changes. +String _outboundTagForProfile(String profileId) { + return singboxOutboundTag(SingboxProxyConnectionId(profileId).encode()); +} + +/// Must mirror Kotlin `SingboxTagFormat.inboundTag(profileId)`. +String _inboundTagForProfile(String profileId) { + return singboxInboundTag(SingboxProxyConnectionId(profileId).encode()); +} + +/// Public so the format-contract test can assert it directly. +String singboxOutboundTag(String runtimeProfileId) => + 'out-${singboxSanitizeTag(runtimeProfileId)}'; + +String singboxInboundTag(String runtimeProfileId) => + 'in-${singboxSanitizeTag(runtimeProfileId)}'; + +String singboxSanitizeTag(String value) => + value.replaceAll(RegExp('[^A-Za-z0-9_.-]'), '_'); diff --git a/apps/weblibre/lib/features/proxy/domain/services/proxy_input_consumer.dart b/apps/weblibre/lib/features/proxy/domain/services/proxy_input_consumer.dart new file mode 100644 index 00000000..4fb2553f --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/proxy_input_consumer.dart @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_specs.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_profile_seed.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_share.dart'; +import 'package:weblibre/features/proxy/data/models/wireguard_config_import.dart'; +import 'package:weblibre/features/proxy/data/parsers/singbox_proxy_uri.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; +import 'package:weblibre/features/user/data/models/proxy_dns_override.dart'; + +part 'proxy_input_consumer.g.dart'; + +enum ProxyFileImportKind { wireguardConf, singboxOutboundJson } + +sealed class ProxyInputOutcome { + const ProxyInputOutcome(); +} + +class ProxyInputImported extends ProxyInputOutcome { + final ProxyProfile created; + + const ProxyInputImported(this.created); +} + +class ProxyInputSeed extends ProxyInputOutcome { + final ProxyProfileSeed seed; + + const ProxyInputSeed(this.seed); +} + +class ProxyInputError extends ProxyInputOutcome { + final String message; + + const ProxyInputError(this.message); +} + +@Riverpod(keepAlive: true) +class ProxyInputConsumer extends _$ProxyInputConsumer { + Future consumeRawText(String raw) async { + final trimmed = raw.trim(); + + if (trimmed.startsWith('$weblibreProxyShareScheme://')) { + return _consumeShareUri(trimmed); + } + + if (_looksLikeWireguardConf(trimmed)) { + return _seedOutcome( + () => _seedFromWireguardConf(trimmed, fileName: 'WireGuard'), + logMessage: 'Failed to parse WireGuard configuration', + ); + } + + if (_looksLikeJsonObject(trimmed)) { + return _seedOutcome( + () => _seedFromSingboxOutboundJson(trimmed, fileName: 'Outbound'), + logMessage: 'Failed to parse pasted sing-box outbound JSON', + ); + } + + return _seedOutcome(() { + final imported = importSingboxProxyUri(trimmed); + return ProxyProfileSeed( + type: imported.type, + name: imported.name, + values: imported.values, + ); + }, logMessage: 'Failed to import proxy URI'); + } + + Future consumeFile( + ProxyFileImportKind kind, + PlatformFile file, + ) async { + final String text; + try { + text = await _readFileText(file); + } catch (error, stackTrace) { + logger.e( + 'Failed to read proxy import file ${file.name}', + error: error, + stackTrace: stackTrace, + ); + return ProxyInputError('Failed to read file: $error'); + } + + return _seedOutcome( + () => switch (kind) { + ProxyFileImportKind.wireguardConf => _seedFromWireguardConf( + text, + fileName: file.name, + ), + ProxyFileImportKind.singboxOutboundJson => _seedFromSingboxOutboundJson( + text, + fileName: file.name, + ), + }, + logMessage: 'Invalid proxy import file ${file.name} ($kind)', + ); + } + + Future _consumeShareUri(String text) async { + try { + final envelope = decodeProxyShareUri(text); + final created = await ref + .read(singboxProxyProfilesRepositoryProvider.notifier) + .createProfile( + name: envelope.name, + type: envelope.type, + configJson: envelope.configJson, + secretJson: envelope.secretJson, + dnsOverrideJson: envelope.dnsOverrideJson, + ); + return ProxyInputImported(created); + } on FormatException catch (error, stackTrace) { + logger.e( + 'Failed to decode WebLibre proxy share URI', + error: error, + stackTrace: stackTrace, + ); + return ProxyInputError(error.message); + } + } + + ProxyInputOutcome _seedOutcome( + ProxyProfileSeed Function() createSeed, { + required String logMessage, + }) { + try { + return ProxyInputSeed(createSeed()); + } on FormatException catch (error, stackTrace) { + logger.e(logMessage, error: error, stackTrace: stackTrace); + return ProxyInputError(error.message); + } + } + + Future _readFileText(PlatformFile file) async { + final bytes = file.bytes; + if (bytes != null) { + return utf8.decode(bytes, allowMalformed: true); + } + final path = file.path; + if (path == null) { + throw const FormatException('Unable to read file contents.'); + } + return File(path).readAsString(); + } + + @override + void build() {} +} + +ProxyProfileSeed _seedFromWireguardConf( + String configText, { + required String fileName, +}) { + final imported = WireguardConfigImport.fromConfigText(configText); + final dnsAddress = imported.primaryDnsAddress; + final dnsOverrideJson = dnsAddress == null + ? null + : jsonEncode(ProxyDnsOverride(remoteServerAddress: dnsAddress).toJson()); + return ProxyProfileSeed( + type: SingboxProxyProfileType.wireguard, + name: _stripExtension(fileName), + values: imported.values, + dnsOverrideJson: dnsOverrideJson, + ); +} + +ProxyProfileSeed _seedFromSingboxOutboundJson( + String text, { + required String fileName, +}) { + final decoded = jsonDecode(text); + if (decoded is! Map) { + throw const FormatException('Expected a sing-box outbound JSON object.'); + } + final outboundType = decoded['type']; + if (outboundType is! String) { + throw const FormatException( + 'Outbound JSON is missing a top-level "type" field.', + ); + } + final spec = singboxProxyFormSpecs.values.firstWhere( + (entry) => entry.outboundType == outboundType, + orElse: () => throw FormatException( + 'No structured form for outbound type "$outboundType". ' + 'Use Custom Outbound JSON instead.', + ), + ); + final values = spec.valuesFromJson(configJson: jsonEncode(decoded)); + return ProxyProfileSeed( + type: spec.type, + name: (decoded['tag'] as String?) ?? _stripExtension(fileName), + values: values, + ); +} + +String _stripExtension(String fileName) { + final dot = fileName.lastIndexOf('.'); + if (dot <= 0) return fileName; + return fileName.substring(0, dot); +} + +bool _looksLikeWireguardConf(String text) { + return text.contains('[Interface]') && + (text.contains('PrivateKey') || text.contains('Address')); +} + +bool _looksLikeJsonObject(String text) { + return text.startsWith('{') && text.endsWith('}'); +} diff --git a/apps/weblibre/lib/features/tor/domain/repositories/tor_proxy.g.dart b/apps/weblibre/lib/features/proxy/domain/services/proxy_input_consumer.g.dart similarity index 65% rename from apps/weblibre/lib/features/tor/domain/repositories/tor_proxy.g.dart rename to apps/weblibre/lib/features/proxy/domain/services/proxy_input_consumer.g.dart index 330c2a77..ca27fb3f 100644 --- a/apps/weblibre/lib/features/tor/domain/repositories/tor_proxy.g.dart +++ b/apps/weblibre/lib/features/proxy/domain/services/proxy_input_consumer.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'tor_proxy.dart'; +part of 'proxy_input_consumer.dart'; // ************************************************************************** // RiverpodGenerator @@ -9,28 +9,28 @@ part of 'tor_proxy.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -@ProviderFor(TorProxyRepository) -final torProxyRepositoryProvider = TorProxyRepositoryProvider._(); +@ProviderFor(ProxyInputConsumer) +final proxyInputConsumerProvider = ProxyInputConsumerProvider._(); -final class TorProxyRepositoryProvider - extends $NotifierProvider { - TorProxyRepositoryProvider._() +final class ProxyInputConsumerProvider + extends $NotifierProvider { + ProxyInputConsumerProvider._() : super( from: null, argument: null, retry: null, - name: r'torProxyRepositoryProvider', + name: r'proxyInputConsumerProvider', isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @override - String debugGetCreateSourceHash() => _$torProxyRepositoryHash(); + String debugGetCreateSourceHash() => _$proxyInputConsumerHash(); @$internal @override - TorProxyRepository create() => TorProxyRepository(); + ProxyInputConsumer create() => ProxyInputConsumer(); /// {@macro riverpod.override_with_value} Override overrideWithValue(void value) { @@ -41,10 +41,10 @@ final class TorProxyRepositoryProvider } } -String _$torProxyRepositoryHash() => - r'83c2976750f3f7907274b1ae4f926cd1de89be83'; +String _$proxyInputConsumerHash() => + r'fed529f253a9bdf72d0b1c23a298765b681d07b8'; -abstract class _$TorProxyRepository extends $Notifier { +abstract class _$ProxyInputConsumer extends $Notifier { void build(); @$mustCallSuper @override diff --git a/apps/weblibre/lib/features/proxy/domain/services/proxy_latency_tester.dart b/apps/weblibre/lib/features/proxy/domain/services/proxy_latency_tester.dart new file mode 100644 index 00000000..1ffd676a --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/proxy_latency_tester.dart @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:socks5_proxy/socks_client.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; +import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; + +part 'proxy_latency_tester.g.dart'; + +/// Mullvad's connectivity check — returns JSON with the egress `ip` plus +/// geolocation. Single round trip gives both reachability and the IP we +/// surface in the chip. Mullvad has a no-logs policy, which fits a +/// privacy-focused browser better than funneling every probe through +/// Cloudflare. +const _probeUrl = 'https://am.i.mullvad.net/json'; + +const _testTimeout = Duration(seconds: 8); + +class ProxyLatencyData with FastEquatable { + final Duration latency; + final int statusCode; + final String? egressIp; + + ProxyLatencyData({ + required this.latency, + required this.statusCode, + this.egressIp, + }); + + @override + List get hashParameters => [latency, statusCode, egressIp]; +} + +/// Per-profile latency results, keyed by profile id. Holds the latest result +/// only; we don't keep history because the test is user-triggered and the user +/// is looking at the chip we render from it. +@Riverpod(keepAlive: true) +class ProxyLatencyResults extends _$ProxyLatencyResults { + @override + Map> build() => const {}; + + void _set(ProxyConnectionId id, AsyncValue result) { + state = {...state, id: result}; + } + + void clear(ProxyConnectionId id) { + if (!state.containsKey(id)) return; + state = { + for (final entry in state.entries) + if (entry.key != id) entry.key: entry.value, + }; + } + + Future _run( + ProxyConnectionId id, + SingboxProxyRuntimeEndpoint endpoint, + ) async { + _set(id, const AsyncLoading()); + final result = await AsyncValue.guard( + () => measureViaSocks(endpoint: endpoint, url: Uri.parse(_probeUrl)), + ); + _set(id, result); + } + + /// Runs a probe through the profile's local SOCKS endpoint and records the + /// result. Profile must already be running — the endpoint is read from the + /// live runtime state. + Future test(String profileId) async { + final runtimeState = ref.read(singboxProxyRuntimeRepositoryProvider).value; + final endpoint = runtimeState?.endpoints.where((endpoint) { + final decoded = ProxyConnectionId.decode(endpoint.profileId); + return decoded is SingboxProxyConnectionId && + decoded.profileId == profileId; + }).firstOrNull; + + if (endpoint == null) { + _set( + SingboxProxyConnectionId(profileId), + AsyncError('Profile is not running', StackTrace.current), + ); + return; + } + + await _run(SingboxProxyConnectionId(profileId), endpoint); + } + + /// Probes Tor's local SOCKS endpoint. Keyed by [TorProxyConnectionId] so the + /// chip and clear/retain logic share a code path with sing-box profiles. + Future testTor() async { + final socksPort = ref.read(torProxyServiceProvider).value?.usableSocksPort; + + if (socksPort == null) { + _set( + const TorProxyConnectionId(), + AsyncError('Tor is not ready', StackTrace.current), + ); + return; + } + + await _run( + const TorProxyConnectionId(), + SingboxProxyRuntimeEndpoint( + profileId: const TorProxyConnectionId().encode(), + host: '127.0.0.1', + port: socksPort, + username: '', + password: '', + ), + ); + } + + /// Drop any cached results for profile ids that are no longer running. + void retainRunning(Set runningIds) { + if (setEquals(runningIds, state.keys.toSet())) return; + + state = { + for (final entry in state.entries) + if (runningIds.contains(entry.key)) entry.key: entry.value, + }; + } +} + +/// Runs a warmup probe (discarded) followed by [sampleCount] timed requests +/// through a single SOCKS5-bound [HttpClient] and reports the minimum RTT — +/// mirrors the speedtest-style "best RTT" reporting used by NekoBox/v2rayN/ +/// clash for user-triggered URL tests. The cold path (TCP + SOCKS5 handshake + +/// upstream outbound warmup) skews the first sample, so we discard it. Reusing +/// the HttpClient lets later samples reuse the pooled SOCKS connection. +/// +/// Throws on failure; the latest successful sample also yields the egress IP +/// parsed from the probe body. +Future measureViaSocks({ + required SingboxProxyRuntimeEndpoint endpoint, + required Uri url, + Duration timeout = _testTimeout, + int sampleCount = 3, +}) async { + final httpClient = HttpClient()..connectionTimeout = timeout; + SocksTCPClient.assignToHttpClient(httpClient, [ + ProxySettings( + InternetAddress(endpoint.host), + endpoint.port, + username: endpoint.username, + password: endpoint.password, + ), + ]); + + try { + // Warmup — result discarded for timing, but if it fails we surface the + // error rather than aggregating min of {failures}. + await _singleProbe(httpClient, url, timeout); + + Duration? best; + var lastStatusCode = 0; + String? lastEgressIp; + Object? lastError; + StackTrace? lastStackTrace; + for (var i = 0; i < sampleCount; i++) { + try { + final probe = await _singleProbe(httpClient, url, timeout); + if (best == null || probe.latency < best) best = probe.latency; + lastStatusCode = probe.statusCode; + lastEgressIp = probe.egressIp ?? lastEgressIp; + } catch (error, stackTrace) { + lastError = error; + lastStackTrace = stackTrace; + } + } + + if (best == null) { + if (lastError != null) { + Error.throwWithStackTrace(lastError, lastStackTrace!); + } + throw const SocketException('No samples completed'); + } + + return ProxyLatencyData( + latency: best, + statusCode: lastStatusCode, + egressIp: lastEgressIp, + ); + } finally { + httpClient.close(force: true); + } +} + +Future _singleProbe( + HttpClient httpClient, + Uri url, + Duration timeout, +) async { + final stopwatch = Stopwatch()..start(); + final request = await httpClient.getUrl(url).timeout(timeout); + final response = await request.close().timeout(timeout); + stopwatch.stop(); + + String? egressIp; + if (response.statusCode == 200) { + final body = await response.transform(utf8.decoder).join(); + egressIp = _parseEgressIp(body); + } else { + await response.drain(); + } + + return ProxyLatencyData( + latency: stopwatch.elapsed, + statusCode: response.statusCode, + egressIp: egressIp, + ); +} + +String? _parseEgressIp(String body) { + try { + final decoded = jsonDecode(body); + if (decoded is Map) { + final ip = decoded['ip']; + if (ip is String && ip.isNotEmpty) return ip; + } + } on FormatException { + // Not JSON — fall through. + } + return null; +} diff --git a/apps/weblibre/lib/features/proxy/domain/services/proxy_latency_tester.g.dart b/apps/weblibre/lib/features/proxy/domain/services/proxy_latency_tester.g.dart new file mode 100644 index 00000000..88161b62 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/proxy_latency_tester.g.dart @@ -0,0 +1,94 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'proxy_latency_tester.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Per-profile latency results, keyed by profile id. Holds the latest result +/// only; we don't keep history because the test is user-triggered and the user +/// is looking at the chip we render from it. + +@ProviderFor(ProxyLatencyResults) +final proxyLatencyResultsProvider = ProxyLatencyResultsProvider._(); + +/// Per-profile latency results, keyed by profile id. Holds the latest result +/// only; we don't keep history because the test is user-triggered and the user +/// is looking at the chip we render from it. +final class ProxyLatencyResultsProvider + extends + $NotifierProvider< + ProxyLatencyResults, + Map> + > { + /// Per-profile latency results, keyed by profile id. Holds the latest result + /// only; we don't keep history because the test is user-triggered and the user + /// is looking at the chip we render from it. + ProxyLatencyResultsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'proxyLatencyResultsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$proxyLatencyResultsHash(); + + @$internal + @override + ProxyLatencyResults create() => ProxyLatencyResults(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue( + Map> value, + ) { + return $ProviderOverride( + origin: this, + providerOverride: + $SyncValueProvider< + Map> + >(value), + ); + } +} + +String _$proxyLatencyResultsHash() => + r'f86dba5c5f17d1f74fdf2b7ca6f5adaa4c42ff75'; + +/// Per-profile latency results, keyed by profile id. Holds the latest result +/// only; we don't keep history because the test is user-triggered and the user +/// is looking at the chip we render from it. + +abstract class _$ProxyLatencyResults + extends $Notifier>> { + Map> build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref< + Map>, + Map> + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + Map>, + Map> + >, + Map>, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/services/singbox_proxy_endpoint_sync.dart b/apps/weblibre/lib/features/proxy/domain/services/singbox_proxy_endpoint_sync.dart new file mode 100644 index 00000000..da0ab24e --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/singbox_proxy_endpoint_sync.dart @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:synchronized/synchronized.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/proxy/data/models/singbox_proxy_profile.dart'; +import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; + +part 'singbox_proxy_endpoint_sync.g.dart'; + +/// Mirrors the sing-box runtime's active SOCKS endpoints into Gecko's +/// container-proxy registry. Listens to [singboxProxyRuntimeRepositoryProvider] +/// and diffs the previously registered set against the current endpoints. +/// +/// Extracted from the runtime repository so that: +/// - the runtime repo only owns process state (start/stop/validate), and +/// - sync is a single side-effect channel — every transition (start, stop, +/// stream-driven refresh, native crash) flows through the same listener, +/// eliminating the duplicate-sync-call hazard that the inline approach had. +/// +/// This is a side-effect-only provider: it must be `keepAlive: true` and is +/// explicitly listened-to from main.dart so the side effect runs without any +/// widget needing to depend on it. +@Riverpod(keepAlive: true) +class SingboxProxyEndpointSync extends _$SingboxProxyEndpointSync { + /// Proxy connection ids most recently registered with Gecko. Used to compute + /// the unregister set on the next sync. + var _registeredProxyIds = {}; + + /// Serialises [_sync] runs so that a fast start→stop→start sequence can't + /// interleave upserts/removals. + final _syncLock = Lock(); + + Future _sync(SingboxProxyRuntimeState runtimeState) async { + await _syncLock.synchronized(() async { + final nextProxyIds = runtimeState.endpoints + .map((endpoint) => endpoint.profileId) + .toSet(); + + final containerProxy = ref.read( + containerProxyRepositoryProvider.notifier, + ); + + for (final proxyId in _registeredProxyIds.difference(nextProxyIds)) { + await containerProxy.removeProxy(proxyId); + } + + if (runtimeState.endpoints.isNotEmpty) { + final profiles = await ref + .read(singboxProxyProfilesRepositoryProvider.notifier) + .fetchProfiles(); + final profileNames = { + for (final profile in profiles) + profile.proxyConnectionId: profile.name, + }; + + for (final endpoint in runtimeState.endpoints) { + await containerProxy.upsertProxy( + GeckoProxySettings( + id: endpoint.profileId, + title: profileNames[endpoint.profileId] ?? endpoint.profileId, + type: 'socks', + host: endpoint.host, + port: endpoint.port, + username: endpoint.username, + password: endpoint.password, + proxyDNS: true, + doNotProxyLocal: true, + ), + ); + } + } + + _registeredProxyIds = nextProxyIds; + }); + } + + @override + void build() { + ref.listen>( + singboxProxyRuntimeRepositoryProvider, + fireImmediately: true, + (previous, next) { + final runtimeState = next.value; + if (runtimeState == null) return; + _sync(runtimeState).catchError((Object error, StackTrace stackTrace) { + logger.e( + 'Failed to sync sing-box proxy endpoints to Gecko', + error: error, + stackTrace: stackTrace, + ); + }); + }, + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/services/singbox_proxy_endpoint_sync.g.dart b/apps/weblibre/lib/features/proxy/domain/services/singbox_proxy_endpoint_sync.g.dart new file mode 100644 index 00000000..db579c0e --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/singbox_proxy_endpoint_sync.g.dart @@ -0,0 +1,116 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'singbox_proxy_endpoint_sync.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Mirrors the sing-box runtime's active SOCKS endpoints into Gecko's +/// container-proxy registry. Listens to [singboxProxyRuntimeRepositoryProvider] +/// and diffs the previously registered set against the current endpoints. +/// +/// Extracted from the runtime repository so that: +/// - the runtime repo only owns process state (start/stop/validate), and +/// - sync is a single side-effect channel — every transition (start, stop, +/// stream-driven refresh, native crash) flows through the same listener, +/// eliminating the duplicate-sync-call hazard that the inline approach had. +/// +/// This is a side-effect-only provider: it must be `keepAlive: true` and is +/// explicitly listened-to from main.dart so the side effect runs without any +/// widget needing to depend on it. + +@ProviderFor(SingboxProxyEndpointSync) +final singboxProxyEndpointSyncProvider = SingboxProxyEndpointSyncProvider._(); + +/// Mirrors the sing-box runtime's active SOCKS endpoints into Gecko's +/// container-proxy registry. Listens to [singboxProxyRuntimeRepositoryProvider] +/// and diffs the previously registered set against the current endpoints. +/// +/// Extracted from the runtime repository so that: +/// - the runtime repo only owns process state (start/stop/validate), and +/// - sync is a single side-effect channel — every transition (start, stop, +/// stream-driven refresh, native crash) flows through the same listener, +/// eliminating the duplicate-sync-call hazard that the inline approach had. +/// +/// This is a side-effect-only provider: it must be `keepAlive: true` and is +/// explicitly listened-to from main.dart so the side effect runs without any +/// widget needing to depend on it. +final class SingboxProxyEndpointSyncProvider + extends $NotifierProvider { + /// Mirrors the sing-box runtime's active SOCKS endpoints into Gecko's + /// container-proxy registry. Listens to [singboxProxyRuntimeRepositoryProvider] + /// and diffs the previously registered set against the current endpoints. + /// + /// Extracted from the runtime repository so that: + /// - the runtime repo only owns process state (start/stop/validate), and + /// - sync is a single side-effect channel — every transition (start, stop, + /// stream-driven refresh, native crash) flows through the same listener, + /// eliminating the duplicate-sync-call hazard that the inline approach had. + /// + /// This is a side-effect-only provider: it must be `keepAlive: true` and is + /// explicitly listened-to from main.dart so the side effect runs without any + /// widget needing to depend on it. + SingboxProxyEndpointSyncProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'singboxProxyEndpointSyncProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$singboxProxyEndpointSyncHash(); + + @$internal + @override + SingboxProxyEndpointSync create() => SingboxProxyEndpointSync(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(void value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$singboxProxyEndpointSyncHash() => + r'2d6b0641db33638b0b339b510dd63cdddab7f7cf'; + +/// Mirrors the sing-box runtime's active SOCKS endpoints into Gecko's +/// container-proxy registry. Listens to [singboxProxyRuntimeRepositoryProvider] +/// and diffs the previously registered set against the current endpoints. +/// +/// Extracted from the runtime repository so that: +/// - the runtime repo only owns process state (start/stop/validate), and +/// - sync is a single side-effect channel — every transition (start, stop, +/// stream-driven refresh, native crash) flows through the same listener, +/// eliminating the duplicate-sync-call hazard that the inline approach had. +/// +/// This is a side-effect-only provider: it must be `keepAlive: true` and is +/// explicitly listened-to from main.dart so the side effect runs without any +/// widget needing to depend on it. + +abstract class _$SingboxProxyEndpointSync extends $Notifier { + void build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + void, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/apps/weblibre/lib/features/proxy/domain/services/subscription_importer.dart b/apps/weblibre/lib/features/proxy/domain/services/subscription_importer.dart new file mode 100644 index 00000000..d7bd6b8c --- /dev/null +++ b/apps/weblibre/lib/features/proxy/domain/services/subscription_importer.dart @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:weblibre/features/proxy/data/parsers/singbox_proxy_uri.dart'; + +/// One line in a subscription that we attempted to parse. Either a usable +/// [SingboxProxyUriImport] or the [FormatException]-style error explaining +/// why the line failed to parse, so the UI can show a per-line outcome +/// instead of silently dropping nodes. +sealed class SubscriptionImportEntry { + final String rawLine; + + const SubscriptionImportEntry({required this.rawLine}); +} + +class SubscriptionEntrySuccess extends SubscriptionImportEntry { + final SingboxProxyUriImport imported; + + const SubscriptionEntrySuccess({ + required super.rawLine, + required this.imported, + }); +} + +class SubscriptionEntryFailure extends SubscriptionImportEntry { + final Object error; + + const SubscriptionEntryFailure({required super.rawLine, required this.error}); +} + +class SubscriptionImportResult { + final List entries; + + const SubscriptionImportResult(this.entries); + + Iterable get successes => + entries.whereType(); + + Iterable get failures => + entries.whereType(); +} + +/// Fetches a v2rayN-style subscription URL and parses its contents. +/// +/// Most subscription servers serve a base64-encoded blob whose decoded body is +/// a newline-delimited list of `ss://`, `vless://`, etc. URIs. Some serve the +/// raw newline-delimited list. We try both: base64 first, then raw, and use +/// whichever produces parseable URIs. +Future fetchSubscription( + Uri url, { + http.Client? client, +}) async { + final ownsClient = client == null; + final actualClient = client ?? http.Client(); + try { + final response = await actualClient + .get(url, headers: {'User-Agent': 'WebLibre/sing-box-subscriber'}) + .timeout(const Duration(seconds: 30)); + if (response.statusCode >= 400) { + throw http.ClientException( + 'Subscription returned HTTP ${response.statusCode}.', + url, + ); + } + return parseSubscriptionBody(response.body); + } finally { + if (ownsClient) actualClient.close(); + } +} + +/// Decodes a subscription body into individual proxy entries. Public so the +/// UI can preview a pasted body without making a network call. +SubscriptionImportResult parseSubscriptionBody(String body) { + final lines = _tryBase64Decode(body) ?? body; + final entries = []; + for (final raw in const LineSplitter().convert(lines)) { + final line = raw.trim(); + if (line.isEmpty || line.startsWith('#')) continue; + try { + entries.add( + SubscriptionEntrySuccess( + rawLine: line, + imported: importSingboxProxyUri(line), + ), + ); + } on FormatException catch (error) { + entries.add(SubscriptionEntryFailure(rawLine: line, error: error)); + } + } + return SubscriptionImportResult(entries); +} + +String? _tryBase64Decode(String body) { + // Subscription bodies are base64 (sometimes URL-safe) without padding. + final stripped = body.replaceAll(RegExp(r'\s'), ''); + if (stripped.isEmpty) return null; + // Only attempt if the body looks like base64 — bail out if it contains + // characters never present in base64 alphabets. + if (!RegExp(r'^[A-Za-z0-9+/_=-]+$').hasMatch(stripped)) return null; + try { + return utf8.decode(base64.decode(base64.normalize(stripped))); + } catch (_) { + try { + return utf8.decode(base64Url.decode(base64Url.normalize(stripped))); + } catch (_) { + return null; + } + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/controllers/ensure_proxy_started.dart b/apps/weblibre/lib/features/proxy/presentation/controllers/ensure_proxy_started.dart new file mode 100644 index 00000000..aee35410 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/controllers/ensure_proxy_started.dart @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; +import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +/// Single entry point that prompts the user to start whichever proxy backend +/// a container is configured to use. No-op when the container has no proxy +/// assigned or when the relevant backend is already running. +Future ensureProxyStartedForContainer( + BuildContext context, + WidgetRef ref, + ContainerData container, +) async { + final proxyConnectionId = container.metadata.proxyConnectionId; + if (proxyConnectionId == null) return true; + + if (proxyConnectionId is TorProxyConnectionId) { + return await _ensureTorStarted(context, ref); + } + + if (proxyConnectionId is SingboxProxyConnectionId) { + return await _maybeStartSingboxProxyForContainer(context, ref, container); + } + + return true; +} + +Future _ensureTorStarted(BuildContext context, WidgetRef ref) async { + final shouldPrompt = await ref + .read(startProxyControllerProvider.notifier) + .shouldPromptProxyStart(); + if (!context.mounted) return false; + if (!shouldPrompt) return true; + + final dialogResult = await showDialog( + context: context, + builder: (context) => const TorDialog(), + ); + + if (dialogResult == true) { + await ref.read(startProxyControllerProvider.notifier).startProxy(); + return true; + } + + return false; +} + +Future _maybeStartSingboxProxyForContainer( + BuildContext context, + WidgetRef ref, + ContainerData container, +) async { + final proxyConnectionId = container.metadata.proxyConnectionId; + if (proxyConnectionId == null) return true; + + if (proxyConnectionId is! SingboxProxyConnectionId) return true; + + // Await any start/stop in flight so we don't prompt the user a second time + // while a start they already triggered is still resolving. If the runtime + // provider is already in AsyncError, keep treating that as "not running" so + // the user can retry from the container entry point. + final runtimeState = ref.read(singboxProxyRuntimeRepositoryProvider); + final resolvedRuntimeState = await switch (runtimeState) { + AsyncData(:final value) => Future.value(value), + AsyncLoading() => _resolveRuntimeStateForPrompt(ref), + AsyncError(:final error, :final stackTrace) => Future.value( + _runtimeStateRetryFallback( + error: error, + stackTrace: stackTrace, + message: + 'Singbox proxy runtime is in error state; continuing so the user can retry startup', + ), + ), + }; + if (!context.mounted) return false; + + final isRunning = resolvedRuntimeState.endpoints.any( + (endpoint) => endpoint.profileId == proxyConnectionId.encode(), + ); + + if (isRunning) return true; + + final proxyTitle = proxyConnectionTitle( + ref.read(proxyConnectionOptionsProvider), + proxyConnectionId, + ); + final shouldStart = await showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon(Icons.route_outlined), + title: const Text('Start Proxy Connection?'), + content: Text( + 'This container uses $proxyTitle, but that connection is not running. Start it now?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Start'), + ), + ], + ), + ); + + if (shouldStart != true) return false; + + try { + await ref + .read(singboxProxyRuntimeRepositoryProvider.notifier) + .startProfile(proxyConnectionId.profileId); + return true; + } catch (error, stackTrace) { + logger.e( + 'Failed to start singbox proxy profile ${proxyConnectionId.profileId}', + error: error, + stackTrace: stackTrace, + ); + if (context.mounted) { + showErrorMessage(context, 'Failed to start proxy: $error'); + } + return false; + } +} + +Future _resolveRuntimeStateForPrompt( + WidgetRef ref, +) async { + try { + return await ref.read(singboxProxyRuntimeRepositoryProvider.future); + } catch (error, stackTrace) { + return _runtimeStateRetryFallback( + error: error, + stackTrace: stackTrace, + message: + 'Singbox proxy runtime is unresolved; continuing so the user can retry startup', + ); + } +} + +SingboxProxyRuntimeState _runtimeStateRetryFallback({ + required Object error, + required StackTrace stackTrace, + required String message, +}) { + logger.w(message, error: error, stackTrace: stackTrace); + return SingboxProxyRuntimeState( + status: SingboxProxyRuntimeStatus.error, + endpoints: const [], + message: error.toString(), + ); +} diff --git a/apps/weblibre/lib/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart b/apps/weblibre/lib/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart new file mode 100644 index 00000000..a038a576 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:async'; +import 'dart:convert'; + +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/uuid.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_specs.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_profile_seed.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_credentials.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; + +part 'proxy_profile_draft_controller.g.dart'; + +const defaultCustomOutboundConfigJson = ''' +{ + "type": "socks", + "server": "127.0.0.1", + "server_port": 1080 +}'''; + +sealed class SaveOutcome { + const SaveOutcome(); +} + +class SaveSucceeded extends SaveOutcome { + const SaveSucceeded(); +} + +class SaveFailed extends SaveOutcome { + final String message; + + const SaveFailed(this.message); +} + +@CopyWith() +class ProxyProfileDraftState with FastEquatable { + final String? profileId; + final ProxyProfile? existingProfile; + final String? loadError; + final String name; + final SingboxProxyProfileType type; + final Map values; + final String? dnsOverrideJson; + final String customConfigJson; + final String customSecretJson; + final bool isSaving; + final bool secretLoaded; + + ProxyProfileDraftState({ + required this.profileId, + required this.existingProfile, + required this.loadError, + required this.name, + required this.type, + required this.values, + required this.dnsOverrideJson, + required this.customConfigJson, + required this.customSecretJson, + required this.isSaving, + required this.secretLoaded, + }); + + factory ProxyProfileDraftState.newProfile({ProxyProfileSeed? seed}) { + final type = seed?.type ?? SingboxProxyProfileType.customOutbound; + return ProxyProfileDraftState( + profileId: null, + existingProfile: null, + loadError: null, + name: seed?.name ?? '', + type: type, + values: _initialValuesForType(type, overlay: seed?.values), + dnsOverrideJson: seed?.dnsOverrideJson, + customConfigJson: defaultCustomOutboundConfigJson, + customSecretJson: '', + isSaving: false, + secretLoaded: true, + ); + } + + factory ProxyProfileDraftState.loadingExisting(String profileId) { + return ProxyProfileDraftState( + profileId: profileId, + existingProfile: null, + loadError: null, + name: '', + type: SingboxProxyProfileType.customOutbound, + values: const {}, + dnsOverrideJson: null, + customConfigJson: defaultCustomOutboundConfigJson, + customSecretJson: '', + isSaving: false, + secretLoaded: false, + ); + } + + bool get isEditing => profileId != null; + + bool get isLoading => + isEditing && existingProfile == null && loadError == null; + + @override + List get hashParameters => [ + profileId, + existingProfile, + loadError, + name, + type, + values, + dnsOverrideJson, + customConfigJson, + customSecretJson, + isSaving, + secretLoaded, + ]; +} + +@riverpod +class ProxyProfileDraft extends _$ProxyProfileDraft { + @override + ProxyProfileDraftState build({String? profileId, ProxyProfileSeed? seed}) { + if (profileId == null) { + return ProxyProfileDraftState.newProfile(seed: seed); + } + + unawaited(_loadExistingProfile(profileId)); + return ProxyProfileDraftState.loadingExisting(profileId); + } + + Future _loadExistingProfile(String profileId) async { + try { + final profile = await ref + .read(singboxProxyProfilesRepositoryProvider.notifier) + .findProfile(profileId); + if (!ref.mounted) return; + + if (profile == null) { + state = state.copyWith( + loadError: 'Proxy profile not found.', + secretLoaded: true, + ); + return; + } + + final secretJson = await ref + .read(singboxProxyCredentialsRepositoryProvider.notifier) + .readSecretJson(profile.id); + if (!ref.mounted) return; + + final spec = singboxProxyFormSpecs[profile.type]; + state = state.copyWith( + existingProfile: profile, + name: profile.name, + type: profile.type, + values: spec == null + ? const {} + : spec.valuesFromJson( + configJson: profile.configJson, + secretJson: secretJson, + ), + dnsOverrideJson: profile.dnsOverrideJson, + customConfigJson: profile.type == SingboxProxyProfileType.customOutbound + ? profile.configJson + : defaultCustomOutboundConfigJson, + customSecretJson: profile.type == SingboxProxyProfileType.customOutbound + ? secretJson ?? '' + : '', + secretLoaded: true, + ); + } catch (error) { + if (!ref.mounted) return; + state = state.copyWith( + loadError: 'Failed to load proxy profile: $error', + secretLoaded: true, + ); + } + } + + void setName(String name) { + state = state.copyWith(name: name); + } + + void setType(SingboxProxyProfileType type) { + if (state.isEditing || state.type == type) return; + state = state.copyWith( + type: type, + values: _initialValuesForType(type), + customConfigJson: defaultCustomOutboundConfigJson, + customSecretJson: '', + secretLoaded: true, + ); + } + + void setFieldValue(String key, String value) { + state = state.copyWith(values: {...state.values, key: value}); + } + + void setDnsOverrideJson(String? json) { + state = state.copyWith.dnsOverrideJson(json); + } + + void setCustomConfigJson(String json) { + state = state.copyWith(customConfigJson: json); + } + + void setCustomSecretJson(String json) { + state = state.copyWith(customSecretJson: json); + } + + Future save() async { + if (state.isSaving) { + return const SaveFailed('Profile is already saving.'); + } + + final draft = state; + final trimmedName = draft.name.trim(); + if (trimmedName.isEmpty) { + return const SaveFailed('Profile name is required.'); + } + + if (draft.isLoading) { + return const SaveFailed('Profile is still loading, please wait.'); + } + + if (draft.loadError != null) { + return SaveFailed(draft.loadError!); + } + + final encoded = _encodeDraft(draft); + switch (encoded) { + case _DraftEncodeFailure(:final message): + return SaveFailed(message); + case _DraftEncodeSuccess(:final configJson, :final secretJson): + state = state.copyWith(isSaving: true); + + try { + final existing = draft.existingProfile; + final profile = ProxyProfile( + id: existing?.id ?? uuid.v4(), + name: trimmedName, + type: draft.type, + configJson: configJson, + dnsOverrideJson: draft.dnsOverrideJson, + createdAt: existing?.createdAt ?? DateTime.now(), + updatedAt: DateTime.now(), + ); + + final validationMessage = await ref + .read(singboxProxyRuntimeRepositoryProvider.notifier) + .validateProfileDraft(profile, secretJson: secretJson); + if (validationMessage != null) { + return SaveFailed(validationMessage); + } + + if (existing == null) { + await ref + .read(singboxProxyProfilesRepositoryProvider.notifier) + .createProfile( + name: trimmedName, + type: draft.type, + configJson: configJson, + secretJson: secretJson, + dnsOverrideJson: draft.dnsOverrideJson, + ); + } else { + await ref + .read(singboxProxyProfilesRepositoryProvider.notifier) + .updateProfile(profile); + await ref + .read(singboxProxyCredentialsRepositoryProvider.notifier) + .writeSecretJson(profile.id, secretJson); + } + + return const SaveSucceeded(); + } catch (error) { + return SaveFailed('Failed to save proxy profile: $error'); + } finally { + if (ref.mounted) { + state = state.copyWith(isSaving: false); + } + } + } + } +} + +sealed class _DraftEncodeResult { + const _DraftEncodeResult(); +} + +class _DraftEncodeSuccess extends _DraftEncodeResult { + final String configJson; + final String? secretJson; + + const _DraftEncodeSuccess({ + required this.configJson, + required this.secretJson, + }); +} + +class _DraftEncodeFailure extends _DraftEncodeResult { + final String message; + + const _DraftEncodeFailure(this.message); +} + +/// Validates and encodes the draft into wire-format JSON in a single pass. +/// For custom-outbound profiles we only check JSON shape; for spec-driven +/// types the spec carries field-level validation. +_DraftEncodeResult _encodeDraft(ProxyProfileDraftState draft) { + if (draft.type == SingboxProxyProfileType.customOutbound) { + final normalizedConfigJson = _normalizeJsonObject(draft.customConfigJson); + if (normalizedConfigJson == null) { + return const _DraftEncodeFailure('Config must be a JSON object.'); + } + + final rawSecret = draft.customSecretJson.trim(); + if (rawSecret.isEmpty) { + return _DraftEncodeSuccess( + configJson: normalizedConfigJson, + secretJson: null, + ); + } + final normalizedSecretJson = _normalizeJsonObject(draft.customSecretJson); + if (normalizedSecretJson == null) { + return const _DraftEncodeFailure('Secrets must be a JSON object.'); + } + return _DraftEncodeSuccess( + configJson: normalizedConfigJson, + secretJson: normalizedSecretJson, + ); + } + + final spec = singboxProxyFormSpecs[draft.type]!; + final validationMessage = spec.validate(draft.values); + if (validationMessage != null) { + return _DraftEncodeFailure(validationMessage); + } + return _DraftEncodeSuccess( + configJson: spec.toConfigJson(draft.values), + secretJson: spec.toSecretJson(draft.values), + ); +} + +String? _normalizeJsonObject(String rawJson) { + try { + final decoded = jsonDecode(rawJson) as Object?; + if (decoded is! Map) { + return null; + } + return const JsonEncoder.withIndent(' ').convert(decoded); + } catch (_) { + return null; + } +} + +Map _initialValuesForType( + SingboxProxyProfileType type, { + Map? overlay, +}) { + final spec = singboxProxyFormSpecs[type]; + if (spec == null) return overlay == null ? const {} : Map.of(overlay); + + return { + for (final field in spec.fields) + if (field.defaultValue != null) field.key: field.defaultValue!, + if (overlay != null) ...overlay, + }; +} diff --git a/apps/weblibre/lib/features/proxy/presentation/controllers/proxy_profile_draft_controller.g.dart b/apps/weblibre/lib/features/proxy/presentation/controllers/proxy_profile_draft_controller.g.dart new file mode 100644 index 00000000..84f0dfdd --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/controllers/proxy_profile_draft_controller.g.dart @@ -0,0 +1,296 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'proxy_profile_draft_controller.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ProxyProfileDraftStateCWProxy { + ProxyProfileDraftState profileId(String? profileId); + + ProxyProfileDraftState existingProfile(ProxyProfile? existingProfile); + + ProxyProfileDraftState loadError(String? loadError); + + ProxyProfileDraftState name(String name); + + ProxyProfileDraftState type(SingboxProxyProfileType type); + + ProxyProfileDraftState values(Map values); + + ProxyProfileDraftState dnsOverrideJson(String? dnsOverrideJson); + + ProxyProfileDraftState customConfigJson(String customConfigJson); + + ProxyProfileDraftState customSecretJson(String customSecretJson); + + ProxyProfileDraftState isSaving(bool isSaving); + + ProxyProfileDraftState secretLoaded(bool secretLoaded); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ProxyProfileDraftState(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// ProxyProfileDraftState(...).copyWith(id: 12, name: "My name") + /// ``` + ProxyProfileDraftState call({ + String? profileId, + ProxyProfile? existingProfile, + String? loadError, + String name, + SingboxProxyProfileType type, + Map values, + String? dnsOverrideJson, + String customConfigJson, + String customSecretJson, + bool isSaving, + bool secretLoaded, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfProxyProfileDraftState.copyWith(...)` or call `instanceOfProxyProfileDraftState.copyWith.fieldName(value)` for a single field. +class _$ProxyProfileDraftStateCWProxyImpl + implements _$ProxyProfileDraftStateCWProxy { + const _$ProxyProfileDraftStateCWProxyImpl(this._value); + + final ProxyProfileDraftState _value; + + @override + ProxyProfileDraftState profileId(String? profileId) => + call(profileId: profileId); + + @override + ProxyProfileDraftState existingProfile(ProxyProfile? existingProfile) => + call(existingProfile: existingProfile); + + @override + ProxyProfileDraftState loadError(String? loadError) => + call(loadError: loadError); + + @override + ProxyProfileDraftState name(String name) => call(name: name); + + @override + ProxyProfileDraftState type(SingboxProxyProfileType type) => call(type: type); + + @override + ProxyProfileDraftState values(Map values) => + call(values: values); + + @override + ProxyProfileDraftState dnsOverrideJson(String? dnsOverrideJson) => + call(dnsOverrideJson: dnsOverrideJson); + + @override + ProxyProfileDraftState customConfigJson(String customConfigJson) => + call(customConfigJson: customConfigJson); + + @override + ProxyProfileDraftState customSecretJson(String customSecretJson) => + call(customSecretJson: customSecretJson); + + @override + ProxyProfileDraftState isSaving(bool isSaving) => call(isSaving: isSaving); + + @override + ProxyProfileDraftState secretLoaded(bool secretLoaded) => + call(secretLoaded: secretLoaded); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ProxyProfileDraftState(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// ProxyProfileDraftState(...).copyWith(id: 12, name: "My name") + /// ``` + ProxyProfileDraftState call({ + Object? profileId = const $CopyWithPlaceholder(), + Object? existingProfile = const $CopyWithPlaceholder(), + Object? loadError = const $CopyWithPlaceholder(), + Object? name = const $CopyWithPlaceholder(), + Object? type = const $CopyWithPlaceholder(), + Object? values = const $CopyWithPlaceholder(), + Object? dnsOverrideJson = const $CopyWithPlaceholder(), + Object? customConfigJson = const $CopyWithPlaceholder(), + Object? customSecretJson = const $CopyWithPlaceholder(), + Object? isSaving = const $CopyWithPlaceholder(), + Object? secretLoaded = const $CopyWithPlaceholder(), + }) { + return ProxyProfileDraftState( + profileId: profileId == const $CopyWithPlaceholder() + ? _value.profileId + // ignore: cast_nullable_to_non_nullable + : profileId as String?, + existingProfile: existingProfile == const $CopyWithPlaceholder() + ? _value.existingProfile + // ignore: cast_nullable_to_non_nullable + : existingProfile as ProxyProfile?, + loadError: loadError == const $CopyWithPlaceholder() + ? _value.loadError + // ignore: cast_nullable_to_non_nullable + : loadError as String?, + name: name == const $CopyWithPlaceholder() || name == null + ? _value.name + // ignore: cast_nullable_to_non_nullable + : name as String, + type: type == const $CopyWithPlaceholder() || type == null + ? _value.type + // ignore: cast_nullable_to_non_nullable + : type as SingboxProxyProfileType, + values: values == const $CopyWithPlaceholder() || values == null + ? _value.values + // ignore: cast_nullable_to_non_nullable + : values as Map, + dnsOverrideJson: dnsOverrideJson == const $CopyWithPlaceholder() + ? _value.dnsOverrideJson + // ignore: cast_nullable_to_non_nullable + : dnsOverrideJson as String?, + customConfigJson: + customConfigJson == const $CopyWithPlaceholder() || + customConfigJson == null + ? _value.customConfigJson + // ignore: cast_nullable_to_non_nullable + : customConfigJson as String, + customSecretJson: + customSecretJson == const $CopyWithPlaceholder() || + customSecretJson == null + ? _value.customSecretJson + // ignore: cast_nullable_to_non_nullable + : customSecretJson as String, + isSaving: isSaving == const $CopyWithPlaceholder() || isSaving == null + ? _value.isSaving + // ignore: cast_nullable_to_non_nullable + : isSaving as bool, + secretLoaded: + secretLoaded == const $CopyWithPlaceholder() || secretLoaded == null + ? _value.secretLoaded + // ignore: cast_nullable_to_non_nullable + : secretLoaded as bool, + ); + } +} + +extension $ProxyProfileDraftStateCopyWith on ProxyProfileDraftState { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfProxyProfileDraftState.copyWith(...)` or `instanceOfProxyProfileDraftState.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ProxyProfileDraftStateCWProxy get copyWith => + _$ProxyProfileDraftStateCWProxyImpl(this); +} + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ProxyProfileDraft) +final proxyProfileDraftProvider = ProxyProfileDraftFamily._(); + +final class ProxyProfileDraftProvider + extends $NotifierProvider { + ProxyProfileDraftProvider._({ + required ProxyProfileDraftFamily super.from, + required ({String? profileId, ProxyProfileSeed? seed}) super.argument, + }) : super( + retry: null, + name: r'proxyProfileDraftProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$proxyProfileDraftHash(); + + @override + String toString() { + return r'proxyProfileDraftProvider' + '' + '$argument'; + } + + @$internal + @override + ProxyProfileDraft create() => ProxyProfileDraft(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ProxyProfileDraftState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is ProxyProfileDraftProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$proxyProfileDraftHash() => r'88dccb74624a8257f77646ab6755dce4e0de465a'; + +final class ProxyProfileDraftFamily extends $Family + with + $ClassFamilyOverride< + ProxyProfileDraft, + ProxyProfileDraftState, + ProxyProfileDraftState, + ProxyProfileDraftState, + ({String? profileId, ProxyProfileSeed? seed}) + > { + ProxyProfileDraftFamily._() + : super( + retry: null, + name: r'proxyProfileDraftProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ProxyProfileDraftProvider call({String? profileId, ProxyProfileSeed? seed}) => + ProxyProfileDraftProvider._( + argument: (profileId: profileId, seed: seed), + from: this, + ); + + @override + String toString() => r'proxyProfileDraftProvider'; +} + +abstract class _$ProxyProfileDraft extends $Notifier { + late final _$args = ref.$arg as ({String? profileId, ProxyProfileSeed? seed}); + String? get profileId => _$args.profileId; + ProxyProfileSeed? get seed => _$args.seed; + + ProxyProfileDraftState build({String? profileId, ProxyProfileSeed? seed}); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + ProxyProfileDraftState, + Object?, + Object? + >; + element.handleCreate( + ref, + () => build(profileId: _$args.profileId, seed: _$args.seed), + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/screens/proxy_routing_settings.dart b/apps/weblibre/lib/features/proxy/presentation/screens/proxy_routing_settings.dart new file mode 100644 index 00000000..fb97c84b --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/screens/proxy_routing_settings.dart @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart'; +import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart'; +import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart'; + +const List proxyRoutingSettingsSections = [ + SettingsSectionDefinition( + title: 'Regular Tabs', + keywords: ['routing'], + entries: [ + SettingsEntryDefinition( + title: 'Regular Tabs Routing Mode', + subtitle: 'Choose how regular tabs are routed through proxies', + keywords: ['container', 'global'], + child: _RegularTabsModeSection(), + ), + SettingsEntryDefinition( + title: 'Proxy for global routing', + subtitle: 'Selected proxy when global routing is enabled', + keywords: ['proxy'], + child: _GlobalRoutingProxySection(), + ), + ], + ), + SettingsSectionDefinition( + title: 'Private Tabs', + keywords: ['private', 'incognito'], + entries: [ + SettingsEntryDefinition( + title: 'Proxy for private tabs', + subtitle: 'Selected proxy that carries private-tab traffic', + keywords: ['proxy'], + child: _PrivateTabsProxySection(), + ), + ], + ), +]; + +class ProxyRoutingSettingsScreen extends StatelessWidget { + const ProxyRoutingSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const SettingsDetailScaffold( + title: 'Proxy Routing', + subtitle: 'Choose which proxy carries regular and private tab traffic.', + icon: Icons.route_outlined, + sections: proxyRoutingSettingsSections, + ); + } +} + +class _RegularTabsModeSection extends ConsumerWidget { + const _RegularTabsModeSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(proxyRoutingSettingsWithDefaultsProvider); + + return RadioGroup( + groupValue: settings.regularTabsMode, + onChanged: (value) async { + if (value != null) { + await ref + .read(proxyRoutingSettingsRepositoryProvider.notifier) + .updateSettings( + (current) => current.copyWith(regularTabsMode: value), + ); + } + }, + child: const Column( + children: [ + RadioListTile.adaptive( + value: ProxyRegularTabRoutingMode.container, + title: Text('Container-Based Routing'), + subtitle: Text( + 'Only tabs in containers with a proxy assigned are routed.', + ), + ), + RadioListTile.adaptive( + value: ProxyRegularTabRoutingMode.all, + title: Text('Global Routing'), + subtitle: Text( + 'Route every regular tab through the selected proxy.', + ), + ), + ], + ), + ); + } +} + +class _GlobalRoutingProxySection extends ConsumerWidget { + const _GlobalRoutingProxySection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(proxyRoutingSettingsWithDefaultsProvider); + if (settings.regularTabsMode != ProxyRegularTabRoutingMode.all) { + return const ListTile( + leading: Icon(Icons.info_outline), + title: Text('Not used in container-based routing'), + subtitle: Text( + 'Switch to global routing above to pick the proxy that carries every regular tab.', + ), + ); + } + + final options = ref.watch(proxyConnectionOptionsProvider); + return _ProxyConnectionPicker( + options: options, + selectedId: settings.regularTabsProxyConnectionId, + onChanged: (id) => ref + .read(proxyRoutingSettingsRepositoryProvider.notifier) + .updateSettings( + (current) => current.copyWith(regularTabsProxyConnectionId: id), + ), + ); + } +} + +class _PrivateTabsProxySection extends ConsumerWidget { + const _PrivateTabsProxySection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(proxyRoutingSettingsWithDefaultsProvider); + final options = ref.watch(proxyConnectionOptionsProvider); + return _ProxyConnectionPicker( + options: options, + selectedId: settings.privateTabsProxyConnectionId, + onChanged: (id) => ref + .read(proxyRoutingSettingsRepositoryProvider.notifier) + .updateSettings( + (current) => current.copyWith(privateTabsProxyConnectionId: id), + ), + ); + } +} + +class _ProxyConnectionPicker extends StatelessWidget { + final List options; + final ProxyConnectionId? selectedId; + final ValueChanged onChanged; + + const _ProxyConnectionPicker({ + required this.options, + required this.selectedId, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final hasUnknownSelection = + selectedId != null && !options.any((option) => option.id == selectedId); + + return RadioGroup( + groupValue: selectedId, + onChanged: onChanged, + child: Column( + children: [ + const RadioListTile.adaptive( + value: null, + title: Text('None'), + subtitle: Text('Use the normal browser connection'), + secondary: Icon(Icons.public), + ), + if (hasUnknownSelection) + ListTile( + leading: Icon( + Icons.warning_amber_outlined, + color: Theme.of(context).colorScheme.error, + ), + title: const Text('Unknown proxy'), + subtitle: const Text('The selected proxy no longer exists.'), + trailing: TextButton( + onPressed: () => onChanged(null), + child: const Text('Clear'), + ), + ), + for (final option in options) + RadioListTile.adaptive( + value: option.id, + title: Text(option.title), + subtitle: Text(option.subtitle), + secondary: const Icon(Icons.route_outlined), + ), + ], + ), + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_logs.dart b/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_logs.dart new file mode 100644 index 00000000..9abbd3c3 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_logs.dart @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_log_message.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_logs.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +class SingboxProxyLogsScreen extends HookConsumerWidget { + const SingboxProxyLogsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final logs = ref.watch(singboxProxyLogsProvider); + final filter = useState(null); + final autoScroll = useState(true); + final scrollController = useScrollController(); + + // Stick to the bottom when new lines arrive — unless the user scrolled up. + // We coalesce scroll-to-bottom across rapid bursts via a pending flag so a + // chatty proxy can't fight the user trying to scroll up. + final pendingAutoScroll = useRef(false); + useEffect(() { + if (!autoScroll.value) return null; + if (pendingAutoScroll.value) return null; + pendingAutoScroll.value = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + pendingAutoScroll.value = false; + if (!autoScroll.value) return; + if (scrollController.hasClients) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + } + }); + return null; + }, [logs.length, autoScroll.value]); + + useEffect(() { + void onScroll() { + if (!scrollController.hasClients) return; + final atBottom = + scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 24; + if (autoScroll.value != atBottom) { + autoScroll.value = atBottom; + } + } + + scrollController.addListener(onScroll); + return () => scrollController.removeListener(onScroll); + }, [scrollController]); + + final filtered = filter.value == null + ? logs + : logs.where((m) => m.level.toLowerCase() == filter.value).toList(); + + return Scaffold( + appBar: AppBar( + title: const Text('Proxy Logs'), + actions: [ + PopupMenuButton( + tooltip: 'Filter by level', + icon: const Icon(Icons.filter_list), + onSelected: (value) => filter.value = value, + itemBuilder: (context) => const [ + PopupMenuItem(child: Text('All levels')), + PopupMenuItem(value: 'error', child: Text('Error')), + PopupMenuItem(value: 'warn', child: Text('Warning')), + PopupMenuItem(value: 'info', child: Text('Info')), + PopupMenuItem(value: 'debug', child: Text('Debug')), + PopupMenuItem(value: 'trace', child: Text('Trace')), + ], + ), + IconButton( + tooltip: 'Copy all', + icon: const Icon(Icons.copy_all), + onPressed: filtered.isEmpty + ? null + : () async { + await Clipboard.setData( + ClipboardData(text: _formatLogs(filtered)), + ); + if (context.mounted) { + showInfoMessage(context, 'Copied to clipboard'); + } + }, + ), + IconButton( + tooltip: 'Share', + icon: const Icon(Icons.share), + onPressed: filtered.isEmpty + ? null + : () => SharePlus.instance.share( + ShareParams( + text: _formatLogs(filtered), + subject: 'proxy logs', + ), + ), + ), + IconButton( + tooltip: 'Clear', + icon: const Icon(Icons.delete_outline), + onPressed: logs.isEmpty + ? null + : () => ref.read(singboxProxyLogsProvider.notifier).clear(), + ), + ], + ), + body: filtered.isEmpty + ? _EmptyLogs(hasFilter: filter.value != null) + : ListView.builder( + controller: scrollController, + itemCount: filtered.length, + itemBuilder: (context, index) => + _LogLine(message: filtered[index]), + ), + ); + } +} + +class _LogLine extends StatelessWidget { + final ProxyLogMessage message; + + const _LogLine({required this.message}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final color = switch (message.level.toLowerCase()) { + 'error' || 'fatal' => scheme.error, + 'warn' || 'warning' => scheme.tertiary, + _ => scheme.onSurface, + }; + final time = DateFormat( + 'HH:mm:ss', + ).format(DateTime.fromMillisecondsSinceEpoch(message.timestamp)); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), + child: SelectableText.rich( + TextSpan( + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + color: color, + ), + children: [ + TextSpan( + text: '$time ', + style: TextStyle(color: scheme.onSurfaceVariant), + ), + TextSpan( + text: '[${_sourceLabel(message.source)}] ', + style: TextStyle(color: scheme.primary), + ), + TextSpan( + text: '[${message.level}] ', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + if (message.profileId != null) + TextSpan( + text: '${message.profileId} ', + style: TextStyle(color: scheme.primary), + ), + TextSpan(text: message.message), + ], + ), + ), + ); + } +} + +class _EmptyLogs extends StatelessWidget { + final bool hasFilter; + + const _EmptyLogs({required this.hasFilter}); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + hasFilter + ? 'No log lines match the current filter.' + : 'No log lines yet. Start a proxy or Tor to see output here.', + textAlign: TextAlign.center, + ), + ), + ); + } +} + +String _formatLogs(List messages) { + final buffer = StringBuffer(); + for (final m in messages) { + final time = DateTime.fromMillisecondsSinceEpoch( + m.timestamp, + ).toIso8601String(); + buffer.writeln( + '$time [${_sourceLabel(m.source)}] [${m.level}]${m.profileId == null ? '' : ' (${m.profileId})'} ${m.message}', + ); + } + return buffer.toString(); +} + +String _sourceLabel(ProxyLogSource source) { + return switch (source) { + ProxyLogSource.singBox => 'sing-box', + ProxyLogSource.tor => 'tor', + }; +} diff --git a/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_profile_editor.dart b/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_profile_editor.dart new file mode 100644 index 00000000..c814daa6 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_profile_editor.dart @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_specs.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_profile_seed.dart'; +import 'package:weblibre/features/proxy/domain/extensions/singbox_proxy_profile_type_x.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_editor/custom_outbound_profile_form.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_editor/profile_dns_override_section.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_editor/profile_editor_section.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart'; +import 'package:weblibre/presentation/widgets/button_spinner.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +class SingboxProxyProfileEditorScreen extends ConsumerWidget { + final String? profileId; + final ProxyProfileSeed? seed; + + const SingboxProxyProfileEditorScreen({super.key, this.profileId, this.seed}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final draftProvider = proxyProfileDraftProvider( + profileId: profileId, + seed: seed, + ); + final draft = ref.watch(draftProvider); + + if (draft.isLoading) { + return Scaffold( + appBar: AppBar(title: const Text('Edit Profile')), + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (draft.loadError != null) { + return Scaffold( + appBar: AppBar(title: const Text('Edit Profile')), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text(draft.loadError!), + ), + ), + ); + } + + return _Editor(draftProvider: draftProvider, draft: draft); + } +} + +class _Editor extends ConsumerWidget { + final ProxyProfileDraftProvider draftProvider; + final ProxyProfileDraftState draft; + + const _Editor({required this.draftProvider, required this.draft}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + Future handleSave() async { + final outcome = await ref.read(draftProvider.notifier).save(); + if (!context.mounted) return; + + switch (outcome) { + case SaveSucceeded(): + Navigator.pop(context); + case SaveFailed(:final message): + showErrorMessage(context, message); + } + } + + final scheme = Theme.of(context).colorScheme; + + return Scaffold( + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: FilledButton.icon( + onPressed: draft.isSaving ? null : handleSave, + icon: draft.isSaving + ? const ButtonSpinner() + : const Icon(Icons.check), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + label: Text(draft.isEditing ? 'Save Changes' : 'Create Profile'), + ), + ), + ), + body: SafeArea( + bottom: false, + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return CustomScrollView( + controller: controller, + slivers: [ + SliverAppBar.large( + centerTitle: false, + title: Text(draft.isEditing ? 'Edit Profile' : 'New Profile'), + ), + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + sliver: SliverList( + delegate: SliverChildListDelegate.fixed([ + ProfileEditorSection( + title: 'General', + child: Padding( + padding: const EdgeInsets.all(16), + child: _GeneralSection( + draftProvider: draftProvider, + draft: draft, + ), + ), + ), + const SizedBox(height: 24), + _ProtocolForm(draftProvider: draftProvider, draft: draft), + const SizedBox(height: 24), + ProfileEditorSection( + title: 'DNS Override', + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + child: ProfileDnsOverrideSection( + draftProvider: draftProvider, + ), + ), + ), + const SizedBox(height: 16), + if (!draft.isEditing) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + 'Tip: use the add menu on the previous screen to ' + 'import from a file, paste a share link, or scan ' + 'a QR code.', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: scheme.onSurfaceVariant), + ), + ), + ]), + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _GeneralSection extends HookConsumerWidget { + final ProxyProfileDraftProvider draftProvider; + final ProxyProfileDraftState draft; + + const _GeneralSection({required this.draftProvider, required this.draft}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final nameController = useTextEditingController(text: draft.name); + useEffect(() { + if (nameController.text != draft.name) { + nameController.text = draft.name; + } + return null; + }, [draft.name]); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: nameController, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Profile Name', + border: OutlineInputBorder(), + ), + onChanged: ref.read(draftProvider.notifier).setName, + ), + const SizedBox(height: 16), + if (draft.isEditing) + // Protocol is locked after creation: each type stores a different + // config/secret JSON shape, so switching mid-edit would silently + // rewrite the profile under a foreign schema. To change protocol, + // create a new profile. + InputDecorator( + decoration: const InputDecoration( + labelText: 'Protocol', + border: OutlineInputBorder(), + helperText: 'Protocol is fixed once a profile is created.', + ), + child: Text(draft.type.label), + ) + else + DropdownButtonFormField( + key: ValueKey(draft.type), + initialValue: draft.type, + decoration: const InputDecoration( + labelText: 'Protocol', + border: OutlineInputBorder(), + ), + items: [ + for (final type in SingboxProxyProfileType.values) + DropdownMenuItem(value: type, child: Text(type.label)), + ], + onChanged: (value) { + if (value != null) { + ref.read(draftProvider.notifier).setType(value); + } + }, + ), + ], + ); + } +} + +class _ProtocolForm extends StatelessWidget { + final ProxyProfileDraftProvider draftProvider; + final ProxyProfileDraftState draft; + + const _ProtocolForm({required this.draftProvider, required this.draft}); + + @override + Widget build(BuildContext context) { + final spec = singboxProxyFormSpecs[draft.type]; + if (spec != null) { + return StructuredProfileForm( + key: ValueKey((draft.type, draft.profileId)), + spec: spec, + draftProvider: draftProvider, + draft: draft, + ); + } + + return CustomOutboundProfileForm( + key: ValueKey(('custom', draft.profileId)), + draftProvider: draftProvider, + draft: draft, + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_profiles.dart b/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_profiles.dart new file mode 100644 index 00000000..fd022860 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/screens/singbox_proxy_profiles.dart @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:async'; + +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/proxy/domain/services/proxy_latency_tester.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/add_proxy_method_sheet.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/profile_tile.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/status_header.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/tor_tile.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; +import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; +import 'package:weblibre/utils/ui_helper.dart'; + +class SingboxProxyProfilesScreen extends HookConsumerWidget { + const SingboxProxyProfilesScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final profilesAsync = ref.watch(singboxProxyProfilesRepositoryProvider); + final runtimeState = ref.watch(singboxProxyRuntimeRepositoryProvider); + final torState = ref.watch(torProxyServiceProvider); + final deletingProfileIds = useState({}); + + final activeProfileIds = _activeProfileIds(runtimeState); + final runtimeBusy = runtimeState.isLoading; + final torIsRunning = torState.value?.isRunning ?? false; + final torIsBusy = torState.isBusy; + + // Drop cached latency results for profiles that are no longer running so a + // stale "120 ms" chip can't outlive its connection. + ref.listen(singboxProxyRuntimeRepositoryProvider, (_, _) { + _pruneLatencyCache(ref); + }); + ref.listen(torProxyServiceProvider, (_, _) { + _pruneLatencyCache(ref); + }); + + return Scaffold( + floatingActionButton: FloatingActionButton.extended( + onPressed: () => unawaited(_showAddSheet(context)), + icon: const Icon(Icons.add), + label: const Text('Add Profile'), + ), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return CustomScrollView( + controller: controller, + slivers: [ + SliverAppBar.large( + centerTitle: false, + title: const Text('Proxy Connections'), + actions: [ + IconButton( + tooltip: 'View logs', + icon: const Icon(Icons.subject), + onPressed: () => + const SingboxProxyLogsRoute().push(context), + ), + ], + ), + ...profilesAsync.when( + data: (profiles) { + // Prune ids whose profile was deleted (or otherwise + // disappeared) so the set can't grow unbounded if a tile + // is unmounted while its delete is still in flight. + final liveProfileIds = { + for (final profile in profiles) profile.id, + }; + final pruned = deletingProfileIds.value.intersection( + liveProfileIds, + ); + if (pruned.length != deletingProfileIds.value.length) { + // Schedule for the next frame to avoid mutating state + // during build. + WidgetsBinding.instance.addPostFrameCallback((_) { + deletingProfileIds.value = pruned; + }); + } + + return [ + _ProfileListBody( + profiles: profiles, + activeProfileIds: activeProfileIds, + deletingProfileIds: pruned, + runtimeBusy: runtimeBusy, + torIsRunning: torIsRunning, + torIsBusy: torIsBusy, + onDeletingChanged: (id, deleting) { + final next = {...deletingProfileIds.value}; + if (deleting) { + next.add(id); + } else { + next.remove(id); + } + deletingProfileIds.value = next; + }, + ), + ]; + }, + loading: () => const [ + SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), + ), + ], + error: (error, stackTrace) { + logger.e( + 'Failed to load singbox proxy profiles', + error: error, + stackTrace: stackTrace, + ); + return [ + SliverFillRemaining( + child: Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: Text( + 'Failed to load proxy profiles:\n$error', + textAlign: TextAlign.center, + ), + ), + ), + ), + ]; + }, + ), + ], + ); + }, + ), + ), + ); + } +} + +Set _activeConnectionIds( + AsyncValue runtimeState, +) { + return runtimeState.asData?.value.endpoints + .map((endpoint) => ProxyConnectionId.decode(endpoint.profileId)) + .nonNulls + .toSet() ?? + const {}; +} + +Set _activeProfileIds( + AsyncValue runtimeState, +) { + return _activeConnectionIds( + runtimeState, + ).whereType().map((id) => id.profileId).toSet(); +} + +void _pruneLatencyCache(WidgetRef ref) { + final runtimeState = ref.read(singboxProxyRuntimeRepositoryProvider); + final torRunning = + ref.read(torProxyServiceProvider).value?.isRunning ?? false; + ref.read(proxyLatencyResultsProvider.notifier).retainRunning({ + ..._activeConnectionIds(runtimeState), + if (torRunning) const TorProxyConnectionId(), + }); +} + +Future _showAddSheet(BuildContext context) async { + final action = await showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (_) => const AddProxyMethodSheet(), + ); + if (action == null) return; + if (!context.mounted) return; + switch (action) { + case AddProxyManual(): + await const SingboxProxyProfileEditorRoute().push(context); + case AddProxySubscription(): + await const SubscriptionImportRoute().push(context); + case AddProxyWithSeed(:final seed): + await SingboxProxyProfileEditorRoute($extra: seed).push(context); + case AddProxyImported(:final message): + showInfoMessage(context, message); + } +} + +class _ProfileListBody extends ConsumerWidget { + final List profiles; + final Set activeProfileIds; + final Set deletingProfileIds; + final bool runtimeBusy; + final bool torIsRunning; + final bool torIsBusy; + final void Function(String id, bool deleting) onDeletingChanged; + + const _ProfileListBody({ + required this.profiles, + required this.activeProfileIds, + required this.deletingProfileIds, + required this.runtimeBusy, + required this.torIsRunning, + required this.torIsBusy, + required this.onDeletingChanged, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final sorted = [...profiles] + ..sort((a, b) { + final aRunning = activeProfileIds.contains(a.id); + final bRunning = activeProfileIds.contains(b.id); + if (aRunning == bRunning) return 0; + return aRunning ? -1 : 1; + }); + + final scheme = Theme.of(context).colorScheme; + final totalRunning = activeProfileIds.length + (torIsRunning ? 1 : 0); + final totalCount = profiles.length + 1; + + Future stopAll() async { + await ref.read(singboxProxyRuntimeRepositoryProvider.notifier).stopAll(); + ref.read(proxyLatencyResultsProvider.notifier).retainRunning(const {}); + if (torIsRunning) { + await ref.read(torProxyServiceProvider.notifier).disconnect(); + } + } + + return SliverList.list( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: StatusHeader( + totalCount: totalCount, + runningCount: totalRunning, + isBusy: runtimeBusy || torIsBusy, + onStopAll: totalRunning == 0 ? null : stopAll, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 12), + child: Text( + 'Profiles', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: scheme.primary, + fontWeight: FontWeight.w700, + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 96), + child: Card.filled( + margin: EdgeInsets.zero, + color: scheme.surfaceContainer, + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + TorProfileTile(isRunning: torIsRunning, isBusy: torIsBusy), + for (final profile in sorted) ...[ + const Divider(height: 1), + ProfileTile( + profile: profile, + isRunning: activeProfileIds.contains(profile.id), + isDeleting: deletingProfileIds.contains(profile.id), + runtimeBusy: runtimeBusy, + onDeletingChanged: onDeletingChanged, + ), + ], + ], + ), + ), + ), + ], + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/screens/subscription_import.dart b/apps/weblibre/lib/features/proxy/presentation/screens/subscription_import.dart new file mode 100644 index 00000000..1ab0afa0 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/screens/subscription_import.dart @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_specs.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/services/subscription_importer.dart'; +import 'package:weblibre/presentation/widgets/button_spinner.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +class SubscriptionImportScreen extends HookConsumerWidget { + const SubscriptionImportScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final urlController = useTextEditingController(); + final hasUrl = useListenableSelector( + urlController, + () => urlController.text.trim().isNotEmpty, + ); + final fetching = useState(false); + final result = useState(null); + final selection = useState({}); + final fetchError = useState(null); + final isImporting = useState(false); + + Future fetch() async { + final raw = urlController.text.trim(); + if (raw.isEmpty) return; + final uri = Uri.tryParse(raw); + if (uri == null || !uri.hasScheme) { + fetchError.value = 'Enter a full https:// subscription URL.'; + return; + } + + fetching.value = true; + fetchError.value = null; + try { + final outcome = await fetchSubscription(uri); + result.value = outcome; + selection.value = { + for (final (index, entry) in outcome.entries.indexed) + if (entry is SubscriptionEntrySuccess) index, + }; + } catch (error, stackTrace) { + logger.e( + 'Failed to fetch subscription from $uri', + error: error, + stackTrace: stackTrace, + ); + fetchError.value = error.toString(); + result.value = null; + } finally { + if (context.mounted) fetching.value = false; + } + } + + Future importSelected() async { + final outcome = result.value; + if (outcome == null) return; + isImporting.value = true; + var imported = 0; + try { + final notifier = ref.read( + singboxProxyProfilesRepositoryProvider.notifier, + ); + for (final (index, entry) in outcome.entries.indexed) { + if (!selection.value.contains(index)) continue; + if (entry is! SubscriptionEntrySuccess) continue; + + final parsed = entry.imported; + final spec = singboxProxyFormSpecs[parsed.type]; + if (spec == null) continue; + await notifier.createProfile( + name: parsed.name ?? 'Imported ${imported + 1}', + type: parsed.type, + configJson: spec.toConfigJson(parsed.values), + secretJson: spec.toSecretJson(parsed.values), + ); + imported++; + } + } finally { + if (context.mounted) isImporting.value = false; + } + + if (context.mounted) { + showInfoMessage(context, 'Imported $imported profile(s)'); + Navigator.of(context).pop(); + } + } + + return Scaffold( + appBar: AppBar(title: const Text('Import Subscription')), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + TextField( + controller: urlController, + keyboardType: TextInputType.url, + decoration: const InputDecoration( + labelText: 'Subscription URL', + hintText: 'https://example.com/sub', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 8), + Text( + 'Supports the v2rayN-style format: a base64-encoded list of ' + 'ss://, vless://, vmess://, trojan://, hysteria2://, tuic:// ' + 'and similar URIs. Routing rules from the subscription are ' + 'ignored — only proxy nodes are imported.', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: !hasUrl || fetching.value ? null : fetch, + icon: fetching.value + ? const ButtonSpinner() + : const Icon(Icons.cloud_download_outlined), + label: const Text('Fetch'), + ), + if (fetchError.value != null) ...[ + const SizedBox(height: 12), + Text( + fetchError.value!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + if (result.value != null) ...[ + const SizedBox(height: 24), + _ResultsSection( + result: result.value!, + selectedIndices: selection.value, + isImporting: isImporting.value, + onSelectionChanged: (next) => selection.value = next, + onImport: importSelected, + ), + ], + ], + ), + ); + } +} + +class _ResultsSection extends StatelessWidget { + final SubscriptionImportResult result; + final Set selectedIndices; + final bool isImporting; + final ValueChanged> onSelectionChanged; + final Future Function() onImport; + + const _ResultsSection({ + required this.result, + required this.selectedIndices, + required this.isImporting, + required this.onSelectionChanged, + required this.onImport, + }); + + @override + Widget build(BuildContext context) { + final successCount = result.successes.length; + final failureCount = result.failures.length; + + void selectAll() { + onSelectionChanged({ + for (final (index, entry) in result.entries.indexed) + if (entry is SubscriptionEntrySuccess) index, + }); + } + + void toggle(int index, bool selected) { + final next = {...selectedIndices}; + if (selected) { + next.add(index); + } else { + next.remove(index); + } + onSelectionChanged(next); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + '$successCount usable node(s)' + '${failureCount > 0 ? ', $failureCount failed' : ''}', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + TextButton( + onPressed: successCount == 0 ? null : selectAll, + child: const Text('Select all'), + ), + TextButton( + onPressed: () => onSelectionChanged(const {}), + child: const Text('Clear'), + ), + ], + ), + const Divider(), + for (final (index, entry) in result.entries.indexed) + _EntryTile( + entry: entry, + selected: selectedIndices.contains(index), + onChanged: entry is SubscriptionEntrySuccess + ? (checked) => toggle(index, checked ?? false) + : null, + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: isImporting || selectedIndices.isEmpty ? null : onImport, + icon: isImporting + ? const ButtonSpinner() + : const Icon(Icons.download_done), + label: Text('Import ${selectedIndices.length} profile(s)'), + ), + ], + ); + } +} + +class _EntryTile extends StatelessWidget { + final SubscriptionImportEntry entry; + final bool selected; + final ValueChanged? onChanged; + + const _EntryTile({ + required this.entry, + required this.selected, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return switch (entry) { + SubscriptionEntrySuccess(:final imported) => CheckboxListTile( + value: selected, + onChanged: onChanged, + title: Text(imported.name ?? entry.rawLine), + subtitle: Text( + imported.type.name, + style: Theme.of(context).textTheme.bodySmall, + ), + dense: true, + ), + SubscriptionEntryFailure(:final error) => ListTile( + leading: Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + ), + title: Text( + entry.rawLine, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + error is FormatException ? error.message : error.toString(), + ), + dense: true, + ), + }; + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/add_proxy_method_sheet.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/add_proxy_method_sheet.dart new file mode 100644 index 00000000..1e350936 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/add_proxy_method_sheet.dart @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_profile_seed.dart'; +import 'package:weblibre/features/proxy/domain/services/proxy_input_consumer.dart'; +import 'package:weblibre/features/qr_scanner/presentation/dialogs/qr_scanner_dialog.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +/// Outcome of the add-proxy bottom sheet. The sheet itself does not navigate +/// or surface success messages: it pops with one of these so the caller can +/// drive navigation from a stable, non-deactivated context. +sealed class AddProxyAction { + const AddProxyAction(); +} + +class AddProxyManual extends AddProxyAction { + const AddProxyManual(); +} + +class AddProxySubscription extends AddProxyAction { + const AddProxySubscription(); +} + +class AddProxyWithSeed extends AddProxyAction { + final ProxyProfileSeed seed; + const AddProxyWithSeed(this.seed); +} + +class AddProxyImported extends AddProxyAction { + final String message; + const AddProxyImported(this.message); +} + +/// Guided bottom sheet shown when the user adds a new proxy profile. Each +/// method either pops with an [AddProxyAction] for the caller to apply or +/// stays open so the user can try another method on error. +class AddProxyMethodSheet extends ConsumerWidget { + const AddProxyMethodSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final scheme = Theme.of(context).colorScheme; + + void popWith(AddProxyAction action) { + if (!context.mounted) return; + Navigator.of(context).pop(action); + } + + Future scanQr() async { + final result = await showDialog( + context: context, + builder: (_) => const QrScannerDialog(), + ); + final code = result?.code?.trim(); + if (code == null || code.isEmpty) return; + if (!context.mounted) return; + final action = await _consumeRawText(context, ref, code); + if (action == null) return; + popWith(action); + } + + Future pasteClipboard() async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + final text = data?.text?.trim(); + if (text == null || text.isEmpty) { + if (context.mounted) { + showInfoMessage(context, 'Clipboard is empty.'); + } + return; + } + if (!context.mounted) return; + final action = await _consumeRawText(context, ref, text); + if (action == null) return; + popWith(action); + } + + Future importFromFile() async { + final kind = await showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (_) => const _FileKindPicker(), + ); + if (kind == null) return; + if (!context.mounted) return; + final action = await _consumeFile(context, ref, kind); + if (action == null) return; + popWith(action); + } + + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 24, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + 'Add Connection', + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600), + ), + ), + const SizedBox(height: 4), + Align( + alignment: Alignment.centerLeft, + child: Text( + 'Choose how you want to add a proxy profile.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 20), + GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.25, + children: [ + _MethodCard( + icon: Icons.content_paste, + title: 'Clipboard', + subtitle: 'Paste share link or URI', + onTap: pasteClipboard, + isPrimary: true, + ), + _MethodCard( + icon: Icons.qr_code_scanner, + title: 'Scan QR', + subtitle: 'From another device', + onTap: scanQr, + ), + _MethodCard( + icon: Icons.cloud_download_outlined, + title: 'Subscription', + subtitle: 'Fetch from URL', + onTap: () => popWith(const AddProxySubscription()), + ), + _MethodCard( + icon: Icons.upload_file_outlined, + title: 'Import file', + subtitle: '.conf or sing-box JSON', + onTap: importFromFile, + ), + ], + ), + const SizedBox(height: 12), + Center( + child: TextButton.icon( + onPressed: () => popWith(const AddProxyManual()), + icon: const Icon(Icons.edit_note), + label: const Text('Enter manually'), + ), + ), + ], + ), + ), + ); + } +} + +class _MethodCard extends StatelessWidget { + final IconData icon; + final String title; + final String subtitle; + final VoidCallback onTap; + final bool isPrimary; + + const _MethodCard({ + required this.icon, + required this.title, + required this.subtitle, + required this.onTap, + this.isPrimary = false, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final background = isPrimary + ? scheme.primaryContainer + : scheme.surfaceContainerHigh; + final iconColor = isPrimary ? scheme.onPrimaryContainer : scheme.primary; + final titleColor = isPrimary ? scheme.onPrimaryContainer : scheme.onSurface; + final subtitleColor = isPrimary + ? scheme.onPrimaryContainer.withValues(alpha: 0.75) + : scheme.onSurfaceVariant; + + return Material( + color: background, + borderRadius: BorderRadius.circular(20), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 28, color: iconColor), + const SizedBox(height: 8), + Text( + title, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: titleColor, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: subtitleColor), + ), + ], + ), + ), + ), + ); + } +} + +class _FileKindPicker extends StatelessWidget { + const _FileKindPicker(); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + 'Import from file', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + ListTile( + leading: const Icon(Icons.vpn_lock), + title: const Text('WireGuard config'), + subtitle: const Text('.conf file with [Interface]/[Peer]'), + onTap: () => + Navigator.of(context).pop(ProxyFileImportKind.wireguardConf), + ), + ListTile( + leading: const Icon(Icons.data_object), + title: const Text('Sing-box outbound JSON'), + subtitle: const Text( + 'Shadowsocks, Trojan, VMess, VLESS, Hysteria, …', + ), + onTap: () => Navigator.of( + context, + ).pop(ProxyFileImportKind.singboxOutboundJson), + ), + ], + ), + ), + ); + } +} + +Future _consumeFile( + BuildContext context, + WidgetRef ref, + ProxyFileImportKind kind, +) async { + final result = await FilePicker.pickFiles(withData: true); + final picked = result?.files.singleOrNull; + if (picked == null) return null; + + final outcome = await ref + .read(proxyInputConsumerProvider.notifier) + .consumeFile(kind, picked); + if (!context.mounted) return null; + return _actionFromOutcome(context, outcome); +} + +Future _consumeRawText( + BuildContext context, + WidgetRef ref, + String rawText, +) async { + final outcome = await ref + .read(proxyInputConsumerProvider.notifier) + .consumeRawText(rawText); + if (!context.mounted) return null; + return _actionFromOutcome(context, outcome); +} + +AddProxyAction? _actionFromOutcome( + BuildContext context, + ProxyInputOutcome outcome, +) { + switch (outcome) { + case ProxyInputImported(:final created): + return AddProxyImported('Imported profile "${created.name}"'); + case ProxyInputSeed(:final seed): + return AddProxyWithSeed(seed); + case ProxyInputError(:final message): + showErrorMessage(context, message); + return null; + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/custom_outbound_profile_form.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/custom_outbound_profile_form.dart new file mode 100644 index 00000000..a62a061d --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/custom_outbound_profile_form.dart @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_editor/profile_editor_section.dart'; +import 'package:weblibre/presentation/widgets/obscurable_text_field.dart'; + +class CustomOutboundProfileForm extends HookConsumerWidget { + final ProxyProfileDraftProvider draftProvider; + final ProxyProfileDraftState draft; + + const CustomOutboundProfileForm({ + super.key, + required this.draftProvider, + required this.draft, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final configController = useTextEditingController( + text: draft.customConfigJson, + ); + final secretController = useTextEditingController( + text: draft.customSecretJson, + ); + + useEffect(() { + if (configController.text != draft.customConfigJson) { + configController.text = draft.customConfigJson; + } + return null; + }, [draft.customConfigJson]); + + useEffect(() { + if (secretController.text != draft.customSecretJson) { + secretController.text = draft.customSecretJson; + } + return null; + }, [draft.customSecretJson]); + + final notifier = ref.read(draftProvider.notifier); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ProfileEditorSection( + title: 'Outbound', + child: Padding( + padding: const EdgeInsets.all(16), + child: TextField( + controller: configController, + minLines: 10, + maxLines: 18, + keyboardType: TextInputType.multiline, + decoration: const InputDecoration( + alignLabelWithHint: true, + labelText: 'Outbound JSON', + helperText: 'Public sing-box outbound object.', + border: OutlineInputBorder(), + ), + onChanged: notifier.setCustomConfigJson, + ), + ), + ), + const SizedBox(height: 24), + ProfileEditorSection( + title: 'Secrets', + child: Padding( + padding: const EdgeInsets.all(16), + child: ObscurableTextField( + controller: secretController, + enabled: draft.secretLoaded, + revealedMinLines: 4, + revealedMaxLines: 10, + decoration: const InputDecoration( + alignLabelWithHint: true, + labelText: 'Secret JSON', + helperText: + 'Optional values merged into the outbound at runtime.', + border: OutlineInputBorder(), + ), + onChanged: notifier.setCustomSecretJson, + ), + ), + ), + ], + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/profile_dns_override_section.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/profile_dns_override_section.dart new file mode 100644 index 00000000..08c928ca --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/profile_dns_override_section.dart @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart'; +import 'package:weblibre/features/user/data/models/proxy_dns_override.dart'; + +/// Per-profile DNS override editor. Keeps the UI surface minimal: a switch to +/// opt in, plus the most common shape (single resolver routed through *this* +/// profile). +class ProfileDnsOverrideSection extends HookConsumerWidget { + final ProxyProfileDraftProvider draftProvider; + + const ProfileDnsOverrideSection({super.key, required this.draftProvider}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final overrideJson = ref.watch( + draftProvider.select((state) => state.dnsOverrideJson), + ); + final initialOverride = useMemoized(() { + if (overrideJson == null || overrideJson.trim().isEmpty) { + return null; + } + try { + return ProxyDnsOverride.fromJson( + jsonDecode(overrideJson) as Map, + ); + } catch (_) { + return null; + } + }, [overrideJson]); + + final enabled = useState(initialOverride != null); + final addressController = useTextEditingController( + text: initialOverride?.remoteServerAddress ?? '', + ); + + // Reseed controls when the parent passes a new override (e.g. the + // WireGuard form populating DNS from an imported `[Interface] DNS = …`). + useEffect(() { + enabled.value = initialOverride != null; + final next = initialOverride?.remoteServerAddress ?? ''; + if (addressController.text != next) { + addressController.text = next; + } + return null; + }, [initialOverride]); + + void emitChange() { + if (!enabled.value) { + ref.read(draftProvider.notifier).setDnsOverrideJson(null); + return; + } + final override = ProxyDnsOverride( + remoteServerAddress: addressController.text.trim().isEmpty + ? null + : addressController.text.trim(), + ); + ref + .read(draftProvider.notifier) + .setDnsOverrideJson(jsonEncode(override.toJson())); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Resolve names through a server reachable inside this profile ' + '(e.g. an internal DoH server behind a corporate WireGuard). ' + 'Leave off to use automatic DNS handling.', + style: Theme.of(context).textTheme.bodySmall, + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + value: enabled.value, + onChanged: (value) { + enabled.value = value; + emitChange(); + }, + title: const Text('Use a profile-specific resolver'), + ), + if (enabled.value) ...[ + const SizedBox(height: 8), + TextField( + controller: addressController, + decoration: const InputDecoration( + labelText: 'DNS server address', + hintText: 'https://10.0.0.1/dns-query', + border: OutlineInputBorder(), + ), + onChanged: (_) => emitChange(), + ), + ], + ], + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/profile_editor_section.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/profile_editor_section.dart new file mode 100644 index 00000000..9315af3c --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/profile_editor_section.dart @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; + +class ProfileEditorSection extends StatelessWidget { + final String title; + final Widget child; + + const ProfileEditorSection({ + super.key, + required this.title, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: scheme.primary, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 12), + Card.filled( + margin: EdgeInsets.zero, + color: scheme.surfaceContainer, + clipBehavior: Clip.antiAlias, + child: child, + ), + ], + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart new file mode 100644 index 00000000..e3048ea9 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_field.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_spec.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_editor/profile_editor_section.dart'; +import 'package:weblibre/presentation/widgets/obscurable_text_field.dart'; + +class StructuredProfileForm extends HookConsumerWidget { + final SingboxProxyFormSpec spec; + final ProxyProfileDraftProvider draftProvider; + final ProxyProfileDraftState draft; + + const StructuredProfileForm({ + super.key, + required this.spec, + required this.draftProvider, + required this.draft, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controllers = useMemoized( + () => { + for (final field in spec.fields) field.key: TextEditingController(), + }, + [spec.type], + ); + + useEffect(() { + return () { + for (final controller in controllers.values) { + controller.dispose(); + } + }; + }, [controllers]); + + useEffect(() { + _syncControllers(controllers, draft.values); + return null; + }, [controllers, draft.values]); + + final sections = _structuredFieldSections(spec.fields); + final notifier = ref.read(draftProvider.notifier); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (final (sectionIndex, section) in sections.indexed) ...[ + if (sectionIndex > 0) const SizedBox(height: 24), + ProfileEditorSection( + title: section.title, + child: Padding( + padding: const EdgeInsets.all(16), + child: _SectionFields( + fields: section.fields, + controllers: controllers, + secretLoaded: draft.secretLoaded, + onChanged: notifier.setFieldValue, + ), + ), + ), + ], + const SizedBox(height: 12), + Text( + 'Advanced protocol options can still be entered with Custom Outbound JSON.', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ); + } +} + +class _SectionFields extends StatelessWidget { + final List fields; + final Map controllers; + final bool secretLoaded; + final void Function(String key, String value) onChanged; + + const _SectionFields({ + required this.fields, + required this.controllers, + required this.secretLoaded, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (final (index, field) in fields.indexed) ...[ + if (index > 0) const SizedBox(height: 16), + if (field.isBoolean) + _BooleanField( + field: field, + controller: controllers[field.key]!, + onChanged: (value) => onChanged(field.key, value), + ) + else if (field.isSecret) + ObscurableTextField( + controller: controllers[field.key], + enabled: secretLoaded, + keyboardType: field.isNumber + ? TextInputType.number + : TextInputType.text, + textInputAction: index == fields.length - 1 + ? TextInputAction.done + : TextInputAction.next, + revealedMinLines: field.key == 'private_key' ? 4 : null, + revealedMaxLines: field.key == 'private_key' ? 8 : 1, + decoration: InputDecoration( + labelText: field.required ? '${field.label} *' : field.label, + helperText: field.helperText ?? 'Stored in secure storage.', + border: const OutlineInputBorder(), + ), + onChanged: (value) => onChanged(field.key, value), + ) + else + TextField( + controller: controllers[field.key], + keyboardType: field.isNumber + ? TextInputType.number + : field.isStringList + ? TextInputType.multiline + : TextInputType.text, + textInputAction: field.isStringList + ? TextInputAction.newline + : index == fields.length - 1 + ? TextInputAction.done + : TextInputAction.next, + minLines: field.isStringList ? 2 : 1, + maxLines: field.isStringList ? 4 : 1, + decoration: InputDecoration( + labelText: field.required ? '${field.label} *' : field.label, + helperText: field.helperText, + border: const OutlineInputBorder(), + ), + onChanged: (value) => onChanged(field.key, value), + ), + ], + ], + ); + } +} + +({String title, List fields}) _section( + String title, + List fields, +) { + return (title: title, fields: fields); +} + +List<({String title, List fields})> +_structuredFieldSections(List fields) { + final basic = []; + final tls = []; + final transport = []; + final multiplex = []; + final dial = []; + final secrets = []; + final protocol = []; + + for (final field in fields) { + if (field.key.startsWith('tls.')) { + tls.add(field); + } else if (field.key.startsWith('transport.')) { + transport.add(field); + } else if (field.key.startsWith('multiplex.')) { + multiplex.add(field); + } else if (_dialFieldKeys.contains(field.key)) { + dial.add(field); + } else if (field.isSecret) { + secrets.add(field); + } else if (_basicFieldKeys.contains(field.key)) { + basic.add(field); + } else { + protocol.add(field); + } + } + + return [ + if (basic.isNotEmpty) _section('Connection', basic), + if (secrets.isNotEmpty) _section('Credentials', secrets), + if (protocol.isNotEmpty) _section('Protocol Options', protocol), + if (tls.isNotEmpty) _section('TLS', tls), + if (transport.isNotEmpty) _section('Transport', transport), + if (multiplex.isNotEmpty) _section('Multiplex', multiplex), + if (dial.isNotEmpty) _section('Dial', dial), + ]; +} + +const _basicFieldKeys = { + 'server', + 'server_port', + 'version', + 'local_address', + 'peer_public_key', +}; +const _dialFieldKeys = { + 'detour', + 'bind_interface', + 'routing_mark', + 'domain_strategy', + 'connect_timeout', +}; + +void _syncControllers( + Map controllers, + Map values, +) { + for (final entry in controllers.entries) { + final next = values[entry.key] ?? ''; + if (entry.value.text != next) { + entry.value.text = next; + } + } +} + +class _BooleanField extends HookWidget { + final SingboxProxyFormField field; + final TextEditingController controller; + final ValueChanged onChanged; + + const _BooleanField({ + required this.field, + required this.controller, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final value = useListenableSelector( + controller, + () => parseFormBool(controller.text), + ); + + return InputDecorator( + decoration: InputDecoration( + labelText: field.required ? '${field.label} *' : field.label, + helperText: field.helperText, + helperMaxLines: 3, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + ), + child: Row( + children: [ + Expanded( + child: Text( + value == null + ? 'Unset (uses default)' + : (value ? 'Enabled' : 'Disabled'), + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + if (value != null) + IconButton( + tooltip: 'Clear', + icon: const Icon(Icons.clear, size: 18), + onPressed: () { + controller.text = ''; + onChanged(''); + }, + ), + Switch( + value: value ?? false, + onChanged: (next) { + final text = next ? 'true' : 'false'; + controller.text = text; + onChanged(text); + }, + ), + ], + ), + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/ip_chip.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/ip_chip.dart new file mode 100644 index 00000000..3eaad8d7 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/ip_chip.dart @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; + +class IpChip extends StatelessWidget { + final String ip; + + const IpChip({super.key, required this.ip}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + + return Tooltip( + message: 'Egress IP $ip', + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: scheme.secondaryContainer, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + ip, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: scheme.onSecondaryContainer, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/latency_chip.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/latency_chip.dart new file mode 100644 index 00000000..97c974ea --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/latency_chip.dart @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/domain/services/proxy_latency_tester.dart'; + +class LatencyChip extends StatelessWidget { + final AsyncValue result; + + const LatencyChip({super.key, required this.result}); + + @override + Widget build(BuildContext context) { + return switch (result) { + AsyncLoading() => const _LatencyStatusChip.loading(), + AsyncError(:final error) => _LatencyStatusChip.error(error), + AsyncData(:final value) => _LatencySuccessChip(value: value), + }; + } +} + +class _LatencyStatusChip extends StatelessWidget { + final String label; + final String tooltip; + final bool isError; + + const _LatencyStatusChip({ + required this.label, + required this.tooltip, + required this.isError, + }); + + const _LatencyStatusChip.loading() + : this( + label: 'Testing...', + tooltip: 'Latency test running', + isError: false, + ); + + _LatencyStatusChip.error(Object error) + : this(label: 'Failed', tooltip: error.toString(), isError: true); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return _LatencyChipContainer( + label: label, + tooltip: tooltip, + backgroundColor: isError + ? scheme.errorContainer + : scheme.surfaceContainerHighest, + foregroundColor: isError + ? scheme.onErrorContainer + : scheme.onSurfaceVariant, + ); + } +} + +class _LatencySuccessChip extends StatelessWidget { + final ProxyLatencyData value; + + const _LatencySuccessChip({required this.value}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final (background, foreground) = _latencyColors(scheme, value.latency); + + return _LatencyChipContainer( + label: '${value.latency.inMilliseconds} ms', + tooltip: 'HTTP ${value.statusCode} in ${value.latency.inMilliseconds} ms', + backgroundColor: background, + foregroundColor: foreground, + ); + } + + static (Color, Color) _latencyColors(ColorScheme scheme, Duration latency) { + final ms = latency.inMilliseconds; + if (ms < 500) return (scheme.primaryContainer, scheme.onPrimaryContainer); + if (ms < 1500) { + return (scheme.tertiaryContainer, scheme.onTertiaryContainer); + } + return (scheme.errorContainer, scheme.onErrorContainer); + } +} + +class _LatencyChipContainer extends StatelessWidget { + final String label; + final String tooltip; + final Color backgroundColor; + final Color foregroundColor; + + const _LatencyChipContainer({ + required this.label, + required this.tooltip, + required this.backgroundColor, + required this.foregroundColor, + }); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + label, + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: foregroundColor), + ), + ), + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/menu_row.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/menu_row.dart new file mode 100644 index 00000000..f8936591 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/menu_row.dart @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; + +class MenuRow extends StatelessWidget { + final IconData icon; + final String label; + + const MenuRow({super.key, required this.icon, required this.label}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [Icon(icon, size: 20), const SizedBox(width: 12), Text(label)], + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/profile_subtitle.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/profile_subtitle.dart new file mode 100644 index 00000000..7f01e6b6 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/profile_subtitle.dart @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/domain/services/proxy_latency_tester.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/ip_chip.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/latency_chip.dart'; + +class ProfileSubtitle extends StatelessWidget { + final String typeLabel; + final AsyncValue? latency; + + const ProfileSubtitle({ + super.key, + required this.typeLabel, + required this.latency, + }); + + @override + Widget build(BuildContext context) { + final latency = this.latency; + if (latency == null) { + return Text(typeLabel); + } + + final egressIp = latency.value?.egressIp; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(typeLabel), + const SizedBox(height: 4), + Wrap( + spacing: 6, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + LatencyChip(result: latency), + if (egressIp != null) IpChip(ip: egressIp), + ], + ), + ], + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/profile_tile.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/profile_tile.dart new file mode 100644 index 00000000..89003ce5 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/profile_tile.dart @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_share.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/extensions/singbox_proxy_profile_type_x.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_credentials.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/proxy/domain/services/proxy_latency_tester.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/menu_row.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/profile_subtitle.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/protocol_badge.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/run_switch.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/share_profile_dialog.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; +import 'package:weblibre/utils/ui_helper.dart'; + +enum ProfileAction { edit, testLatency, share, delete } + +class ProfileTile extends ConsumerWidget { + final ProxyProfile profile; + final bool isRunning; + final bool isDeleting; + final bool runtimeBusy; + final void Function(String profileId, bool deleting) onDeletingChanged; + + const ProfileTile({ + super.key, + required this.profile, + required this.isRunning, + required this.isDeleting, + required this.runtimeBusy, + required this.onDeletingChanged, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isBusy = runtimeBusy || isDeleting; + final latencyResult = ref.watch( + proxyLatencyResultsProvider.select( + (map) => map[SingboxProxyConnectionId(profile.id)], + ), + ); + + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + leading: isDeleting + ? const SizedBox.square( + dimension: 36, + child: Padding( + padding: EdgeInsets.all(6), + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : ProtocolBadge(type: profile.type, active: isRunning), + title: Text( + profile.name, + style: TextStyle( + fontWeight: isRunning ? FontWeight.w600 : FontWeight.w500, + ), + ), + subtitle: ProfileSubtitle( + typeLabel: profile.type.label, + latency: latencyResult, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + PopupMenuButton( + enabled: !isBusy, + onSelected: (action) => _onAction(context, ref, action), + itemBuilder: (context) => [ + const PopupMenuItem( + value: ProfileAction.edit, + child: MenuRow(icon: Icons.edit_outlined, label: 'Edit'), + ), + PopupMenuItem( + value: ProfileAction.testLatency, + enabled: isRunning, + child: MenuRow( + icon: latencyResult is AsyncLoading + ? Icons.hourglass_bottom + : Icons.network_check, + label: 'Test connection', + ), + ), + const PopupMenuItem( + value: ProfileAction.share, + child: MenuRow(icon: Icons.share_outlined, label: 'Share'), + ), + const PopupMenuItem( + value: ProfileAction.delete, + child: MenuRow(icon: Icons.delete_outline, label: 'Delete'), + ), + ], + ), + RunSwitch( + isRunning: isRunning, + disabled: isBusy, + onTap: () => _toggleRunState(context, ref), + ), + ], + ), + onTap: () => + SingboxProxyProfileEditorRoute(profileId: profile.id).push(context), + ); + } + + Future _toggleRunState(BuildContext context, WidgetRef ref) async { + try { + final notifier = ref.read(singboxProxyRuntimeRepositoryProvider.notifier); + if (isRunning) { + await notifier.stopProfiles([profile.id]); + ref + .read(proxyLatencyResultsProvider.notifier) + .clear(SingboxProxyConnectionId(profile.id)); + } else { + await notifier.startProfile(profile.id); + } + } catch (error, stackTrace) { + logger.e( + 'Failed to toggle singbox proxy run state for ${profile.id}', + error: error, + stackTrace: stackTrace, + ); + if (context.mounted) { + showErrorMessage( + context, + isRunning + ? 'Failed to stop proxy: $error' + : 'Failed to start proxy: $error', + ); + } + } + } + + Future _onAction( + BuildContext context, + WidgetRef ref, + ProfileAction action, + ) async { + switch (action) { + case ProfileAction.edit: + await _handleEdit(context); + case ProfileAction.testLatency: + await _handleTestLatency(ref); + case ProfileAction.share: + await _handleShare(context, ref); + case ProfileAction.delete: + await _handleDelete(context, ref); + } + } + + Future _handleEdit(BuildContext context) { + return SingboxProxyProfileEditorRoute(profileId: profile.id).push(context); + } + + Future _handleTestLatency(WidgetRef ref) { + return ref.read(proxyLatencyResultsProvider.notifier).test(profile.id); + } + + Future _handleShare(BuildContext context, WidgetRef ref) async { + final secret = await ref + .read(singboxProxyCredentialsRepositoryProvider.notifier) + .readSecretJson(profile.id); + final shareUri = encodeProxyShareUri( + ProxyShareEnvelope( + name: profile.name, + type: profile.type, + configJson: profile.configJson, + secretJson: secret, + dnsOverrideJson: profile.dnsOverrideJson, + ), + ); + if (!context.mounted) return; + await showDialog( + context: context, + builder: (context) => + ShareProfileDialog(profileName: profile.name, shareUri: shareUri), + ); + } + + Future _handleDelete(BuildContext context, WidgetRef ref) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Profile?'), + content: Text( + isRunning + ? 'Stop ${profile.name}, then delete it and its stored secrets? Tabs and containers assigned to this profile will be blocked until you choose another proxy or clear the assignment.' + : 'Delete ${profile.name} and its stored secrets? Tabs and containers assigned to this profile will be blocked until you choose another proxy or clear the assignment.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(isRunning ? 'Stop and Delete' : 'Delete'), + ), + ], + ), + ); + if (confirmed != true) return; + + onDeletingChanged(profile.id, true); + try { + await ref + .read(singboxProxyRuntimeRepositoryProvider.notifier) + .deleteProfile(profile.id); + ref + .read(proxyLatencyResultsProvider.notifier) + .clear(SingboxProxyConnectionId(profile.id)); + } catch (error, stackTrace) { + logger.e( + 'Failed to delete singbox proxy profile ${profile.id}', + error: error, + stackTrace: stackTrace, + ); + if (context.mounted) { + showErrorMessage(context, 'Failed to delete profile: $error'); + } + } finally { + if (context.mounted) onDeletingChanged(profile.id, false); + } + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/protocol_badge.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/protocol_badge.dart new file mode 100644 index 00000000..3f64aece --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/protocol_badge.dart @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:weblibre/features/proxy/domain/extensions/singbox_proxy_profile_type_x.dart'; + +class ProtocolBadge extends StatelessWidget { + final SingboxProxyProfileType type; + final bool active; + + const ProtocolBadge({super.key, required this.type, required this.active}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final background = active + ? scheme.primary.withValues(alpha: 0.15) + : scheme.surfaceContainerHighest; + final foreground = active ? scheme.primary : scheme.onSurfaceVariant; + + return Container( + width: 44, + height: 44, + alignment: Alignment.center, + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + type.badge, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: foreground, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/run_switch.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/run_switch.dart new file mode 100644 index 00000000..4b9c560f --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/run_switch.dart @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; + +class RunSwitch extends StatelessWidget { + final bool isRunning; + final bool disabled; + final VoidCallback onTap; + + const RunSwitch({ + super.key, + required this.isRunning, + required this.disabled, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final background = isRunning + ? scheme.primary + : scheme.surfaceContainerHighest; + final foreground = isRunning ? scheme.onPrimary : scheme.onSurface; + + return IconButton.filled( + tooltip: isRunning ? 'Stop' : 'Start', + onPressed: disabled ? null : onTap, + style: IconButton.styleFrom( + backgroundColor: background, + foregroundColor: foreground, + disabledBackgroundColor: scheme.surfaceContainerHighest.withValues( + alpha: 0.5, + ), + ), + icon: Icon(isRunning ? Icons.stop_rounded : Icons.play_arrow_rounded), + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/share_profile_dialog.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/share_profile_dialog.dart new file mode 100644 index 00000000..be872b22 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/share_profile_dialog.dart @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +class ShareProfileDialog extends StatelessWidget { + final String profileName; + final String shareUri; + + const ShareProfileDialog({ + super.key, + required this.profileName, + required this.shareUri, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Share "$profileName"'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.warning_amber_outlined, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'This link contains the full profile, including any ' + 'stored credentials. Share carefully.', + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(maxHeight: 160), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + child: SelectableText( + shareUri, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), + ), + ), + ), + ], + ), + actions: [ + TextButton.icon( + icon: const Icon(Icons.copy), + label: const Text('Copy'), + onPressed: () async { + await Clipboard.setData(ClipboardData(text: shareUri)); + if (context.mounted) { + showInfoMessage(context, 'Copied to clipboard'); + } + }, + ), + TextButton.icon( + icon: const Icon(Icons.share), + label: const Text('Share'), + onPressed: () async { + await SharePlus.instance.share( + ShareParams(text: shareUri, subject: profileName), + ); + }, + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/status_header.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/status_header.dart new file mode 100644 index 00000000..cfc111e0 --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/status_header.dart @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; + +class StatusHeader extends StatelessWidget { + final int totalCount; + final int runningCount; + final bool isBusy; + final VoidCallback? onStopAll; + + const StatusHeader({ + super.key, + required this.totalCount, + required this.runningCount, + required this.isBusy, + required this.onStopAll, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final isAnyRunning = runningCount > 0; + + final background = isAnyRunning + ? scheme.primaryContainer + : scheme.surfaceContainerHigh; + final onBackground = isAnyRunning + ? scheme.onPrimaryContainer + : scheme.onSurfaceVariant; + + return Container( + padding: const EdgeInsets.fromLTRB(16, 14, 12, 14), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: onBackground.withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + child: Icon( + isAnyRunning ? Icons.cloud_done : Icons.cloud_off_outlined, + color: onBackground, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isAnyRunning ? 'Active' : 'Disconnected', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: onBackground, + ), + ), + Text( + isAnyRunning + ? '$runningCount of $totalCount routing traffic' + : 'Tap a profile to connect', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: onBackground.withValues(alpha: 0.8), + ), + ), + ], + ), + ), + if (onStopAll != null) + IconButton.filled( + tooltip: 'Stop all', + onPressed: isBusy ? null : onStopAll, + style: IconButton.styleFrom( + backgroundColor: scheme.errorContainer, + foregroundColor: scheme.onErrorContainer, + ), + icon: const Icon(Icons.stop_rounded), + ), + ], + ), + ); + } +} diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/tor_tile.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/tor_tile.dart new file mode 100644 index 00000000..a2c11ccb --- /dev/null +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_list/tor_tile.dart @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/services/proxy_latency_tester.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/menu_row.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/profile_subtitle.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/run_switch.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; +import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; +import 'package:weblibre/presentation/icons/tor_icons.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +enum TorAction { edit, testLatency } + +class TorProfileTile extends ConsumerWidget { + final bool isRunning; + final bool isBusy; + + const TorProfileTile({ + super.key, + required this.isRunning, + required this.isBusy, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final scheme = Theme.of(context).colorScheme; + final background = isRunning + ? scheme.primary.withValues(alpha: 0.15) + : scheme.surfaceContainerHighest; + final foreground = isRunning ? scheme.primary : scheme.onSurfaceVariant; + final latencyResult = ref.watch( + proxyLatencyResultsProvider.select( + (map) => map[const TorProxyConnectionId()], + ), + ); + final torReady = ref.watch( + torProxyServiceProvider.select((s) => s.isReady), + ); + + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + leading: Container( + width: 44, + height: 44, + alignment: Alignment.center, + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(TorIcons.onionAlt, color: foreground, size: 24), + ), + title: Text( + 'Tor', + style: TextStyle( + fontWeight: isRunning ? FontWeight.w600 : FontWeight.w500, + ), + ), + subtitle: ProfileSubtitle( + typeLabel: 'Onion routing', + latency: latencyResult, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + PopupMenuButton( + enabled: !isBusy, + onSelected: (action) async { + switch (action) { + case TorAction.edit: + await const TorProxyRoute().push(context); + case TorAction.testLatency: + await ref + .read(proxyLatencyResultsProvider.notifier) + .testTor(); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: TorAction.edit, + child: MenuRow(icon: Icons.edit_outlined, label: 'Edit'), + ), + PopupMenuItem( + value: TorAction.testLatency, + enabled: torReady, + child: MenuRow( + icon: latencyResult is AsyncLoading + ? Icons.hourglass_bottom + : Icons.network_check, + label: 'Test connection', + ), + ), + ], + ), + RunSwitch( + isRunning: isRunning, + disabled: isBusy, + onTap: () => _toggle(context, ref), + ), + ], + ), + onTap: () => const TorProxyRoute().push(context), + ); + } + + Future _toggle(BuildContext context, WidgetRef ref) async { + try { + final service = ref.read(torProxyServiceProvider.notifier); + if (isRunning) { + await service.disconnect(); + ref + .read(proxyLatencyResultsProvider.notifier) + .clear(const TorProxyConnectionId()); + } else { + await service.startOrReconfigure(reconfigureIfRunning: false); + } + } catch (error, stackTrace) { + logger.e( + 'Failed to toggle Tor proxy', + error: error, + stackTrace: stackTrace, + ); + if (context.mounted) { + showErrorMessage( + context, + isRunning + ? 'Failed to stop Tor: $error' + : 'Failed to start Tor: $error', + ); + } + } + } +} diff --git a/apps/weblibre/lib/features/search_credits/data/models/web_search_settings.dart b/apps/weblibre/lib/features/search_credits/data/models/web_search_settings.dart index 24b6785b..fd0a2542 100644 --- a/apps/weblibre/lib/features/search_credits/data/models/web_search_settings.dart +++ b/apps/weblibre/lib/features/search_credits/data/models/web_search_settings.dart @@ -20,7 +20,7 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; part 'web_search_settings.g.dart'; diff --git a/apps/weblibre/lib/features/search_credits/domain/providers/proxy_client.dart b/apps/weblibre/lib/features/search_credits/domain/providers/proxy_client.dart index e37fc7f1..5d96d28b 100644 --- a/apps/weblibre/lib/features/search_credits/domain/providers/proxy_client.dart +++ b/apps/weblibre/lib/features/search_credits/domain/providers/proxy_client.dart @@ -25,6 +25,7 @@ import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:socks5_proxy/socks_client.dart'; import 'package:weblibre/features/search_credits/domain/repositories/web_search_settings.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; part 'proxy_client.g.dart'; @@ -39,11 +40,7 @@ int? searchProxyPort(Ref ref) { webSearchSettingsControllerProvider.select((s) => s.routeThroughTor), ); if (!route) return null; - final status = ref.watch(torProxyServiceProvider).value; - if (status == null || !status.isRunning || status.bootstrapProgress < 100) { - return null; - } - return status.socksPort; + return ref.watch(torProxyServiceProvider).value?.usableSocksPort; } /// HttpClient used by both WebSocket (via `IOWebSocketChannel.customClient`) diff --git a/apps/weblibre/lib/features/search_credits/domain/providers/proxy_client.g.dart b/apps/weblibre/lib/features/search_credits/domain/providers/proxy_client.g.dart index d051712a..c20dbf61 100644 --- a/apps/weblibre/lib/features/search_credits/domain/providers/proxy_client.g.dart +++ b/apps/weblibre/lib/features/search_credits/domain/providers/proxy_client.g.dart @@ -61,7 +61,7 @@ final class SearchProxyPortProvider } } -String _$searchProxyPortHash() => r'759420a65b9a1375ff9595de2a0dcbf174ae3e79'; +String _$searchProxyPortHash() => r'1ca1d6f08f9eeabf2f70ce2e27b71242e0808dc0'; /// HttpClient used by both WebSocket (via `IOWebSocketChannel.customClient`) /// and HTTP-based search clients. SOCKS5-routed when Tor toggle is on, plain diff --git a/apps/weblibre/lib/features/search_credits/domain/repositories/web_search_settings.dart b/apps/weblibre/lib/features/search_credits/domain/repositories/web_search_settings.dart index f8f92f60..e2362135 100644 --- a/apps/weblibre/lib/features/search_credits/domain/repositories/web_search_settings.dart +++ b/apps/weblibre/lib/features/search_credits/domain/repositories/web_search_settings.dart @@ -21,7 +21,7 @@ import 'package:riverpod/experimental/persist.dart'; import 'package:riverpod_annotation/experimental/json_persist.dart'; import 'package:riverpod_annotation/experimental/persist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:weblibre/features/search_credits/data/models/web_search_settings.dart'; import 'package:weblibre/features/user/data/providers.dart'; diff --git a/apps/weblibre/lib/features/settings/presentation/screens/experimental_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/experimental_settings.dart index 3cf07922..f7ccb856 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/experimental_settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/experimental_settings.dart @@ -27,6 +27,7 @@ import 'package:weblibre/features/user/data/models/engine_settings.dart'; import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart'; import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; import 'package:weblibre/utils/exit_app.dart'; +import 'package:weblibre/utils/ui_helper.dart'; const List experimentalSettingsSections = [ SettingsSectionDefinition( @@ -86,7 +87,6 @@ class _UnifiedPushDistributorTile extends StatelessWidget { ), trailing: const Icon(Icons.chevron_right), onTap: () async { - final messenger = ScaffoldMessenger.of(context); final success = await GeckoBrowserService() .pickUnifiedPushDistributor(); @@ -94,15 +94,14 @@ class _UnifiedPushDistributorTile extends StatelessWidget { return; } - messenger.showSnackBar( - SnackBar( - content: Text( - success - ? 'UnifiedPush distributor configured.' - : 'Could not configure UnifiedPush. Install a distributor and try again.', - ), - ), - ); + if (success) { + showInfoMessage(context, 'UnifiedPush distributor configured.'); + } else { + showErrorMessage( + context, + 'Could not configure UnifiedPush. Install a distributor and try again.', + ); + } }, ); } diff --git a/apps/weblibre/lib/features/settings/presentation/screens/locale_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/locale_settings.dart index bb6ca70f..0802e782 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/locale_settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/locale_settings.dart @@ -55,12 +55,11 @@ class LocaleSettingsScreen extends HookConsumerWidget { final availableLocales = useMemoized( () => { - ...systemLocales, - ...userLocales.value, - Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), - }.toList()..sort( - (a, b) => a.toLanguageTag().compareTo(b.toLanguageTag()), - ), + ...systemLocales, + ...userLocales.value, + Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), + }.toList() + ..sort((a, b) => a.toLanguageTag().compareTo(b.toLanguageTag())), [systemLocales, userLocales], ); @@ -69,10 +68,9 @@ class LocaleSettingsScreen extends HookConsumerWidget { final filteredLocales = availableLocales.where((locale) { if (search.normalizedQuery.isEmpty) return true; - return locale - .toLanguageTag() - .toLowerCase() - .contains(search.normalizedQuery); + return locale.toLanguageTag().toLowerCase().contains( + search.normalizedQuery, + ); }).toList(); return SettingsCustomScrollScaffold( diff --git a/apps/weblibre/lib/features/settings/presentation/screens/proxy_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/proxy_settings.dart new file mode 100644 index 00000000..5904a324 --- /dev/null +++ b/apps/weblibre/lib/features/settings/presentation/screens/proxy_settings.dart @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart'; + +const List proxySettingsSections = [ + SettingsSectionDefinition( + title: 'Proxy', + entries: [ + SettingsEntryDefinition( + title: 'Proxy Connections', + subtitle: 'Manage proxy profiles and connections', + keywords: [ + 'sing-box', + 'socks', + 'vpn', + 'wireguard', + 'tor', + 'onion', + 'bridges', + 'obfs4', + 'snowflake', + ], + child: _ProxyConnectionsTile(), + ), + SettingsEntryDefinition( + title: 'Proxy Routing', + subtitle: 'Choose which proxy carries regular and private tabs', + keywords: ['routing', 'container'], + child: _ProxyRoutingTile(), + ), + ], + ), +]; + +class ProxySettingsScreen extends StatelessWidget { + const ProxySettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const SettingsDetailScaffold( + title: 'Proxy', + subtitle: 'Manage proxy connections and choose which tabs use them.', + icon: MdiIcons.lanConnect, + sections: proxySettingsSections, + ); + } +} + +class _ProxyConnectionsTile extends StatelessWidget { + const _ProxyConnectionsTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(MdiIcons.lanConnect), + title: const Text('Proxy Connections'), + subtitle: const Text('Manage proxy profiles and connections'), + trailing: const Icon(Icons.chevron_right), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + onTap: () async { + await const SingboxProxyProfilesRoute().push(context); + }, + ); + } +} + +class _ProxyRoutingTile extends StatelessWidget { + const _ProxyRoutingTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.route_outlined), + title: const Text('Proxy Routing'), + subtitle: const Text( + 'Choose which proxy carries regular and private tabs', + ), + trailing: const Icon(Icons.chevron_right), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + onTap: () async { + await const ProxyRoutingSettingsRoute().push(context); + }, + ); + } +} diff --git a/apps/weblibre/lib/features/settings/presentation/screens/settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/settings.dart index e6b348c9..80d9fa8e 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/settings.dart @@ -27,6 +27,7 @@ import 'package:weblibre/features/settings/presentation/screens/browsing_setting import 'package:weblibre/features/settings/presentation/screens/extensions_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/general_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/privacy_security_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/proxy_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/search_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/web_content_settings.dart'; import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart'; @@ -42,7 +43,10 @@ class SettingsScreen extends HookWidget { final categories = _buildCategories(); final sections = search.normalizedQuery.isEmpty ? _buildCategorySections(categories) - : _buildSearchSections(categories, search.normalizedQuery); + : _buildSearchSections([ + ...categories.browser, + ...categories.services, + ], search.normalizedQuery); return Scaffold( body: SafeArea( @@ -83,8 +87,13 @@ class SettingsScreen extends HookWidget { } } -List<_SettingsCategoryDefinition> _buildCategories() { - return [ +typedef _CategoryGroups = ({ + List<_SettingsCategoryDefinition> browser, + List<_SettingsCategoryDefinition> services, +}); + +_CategoryGroups _buildCategories() { + final browser = [ _SettingsCategoryDefinition( title: 'General', subtitle: 'Appearance, language, downloads', @@ -139,6 +148,26 @@ List<_SettingsCategoryDefinition> _buildCategories() { sections: privacySecuritySettingsSections, onTap: (context) => PrivacySecuritySettingsRoute().push(context), ), + _SettingsCategoryDefinition( + title: 'Proxy', + subtitle: 'Connections and routing', + icon: MdiIcons.lanConnect, + keywords: const [ + 'proxy', + 'sing-box', + 'socks', + 'vpn', + 'wireguard', + 'routing', + 'tor', + 'container', + ], + sections: proxySettingsSections, + onTap: (context) => const ProxySettingsRoute().push(context), + ), + ]; + + final services = [ _SettingsCategoryDefinition( title: 'Extensions', subtitle: 'Install and manage extension sources', @@ -170,23 +199,25 @@ List<_SettingsCategoryDefinition> _buildCategories() { onTap: (context) => AdvancedSettingsRoute().push(context), ), ]; + + return (browser: browser, services: services); } List _buildCategorySections( - List<_SettingsCategoryDefinition> categories, + _CategoryGroups categories, ) { return [ SettingsSectionDefinition( title: 'Browser', entries: [ - for (final category in categories.take(7)) + for (final category in categories.browser) _buildCategoryEntry(category), ], ), SettingsSectionDefinition( title: 'Services & Advanced', entries: [ - for (final category in categories.skip(7)) + for (final category in categories.services) _buildCategoryEntry(category), ], ), diff --git a/apps/weblibre/lib/features/settings/presentation/screens/ublock_filter_lists.dart b/apps/weblibre/lib/features/settings/presentation/screens/ublock_filter_lists.dart index 429af950..74a1e87b 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/ublock_filter_lists.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/ublock_filter_lists.dart @@ -601,8 +601,7 @@ class _GroupCard extends StatelessWidget { for (final entry in parentTree.entries) { final parentMatches = - groupMatches || - (entry.key?.toLowerCase().contains(query) ?? false); + groupMatches || (entry.key?.toLowerCase().contains(query) ?? false); final filteredTokens = entry.value.where((tokenKey) { if (query.isEmpty || parentMatches) return true; final asset = registry[tokenKey]; diff --git a/apps/weblibre/lib/features/small_web/presentation/widgets/small_web_browser_overlay.dart b/apps/weblibre/lib/features/small_web/presentation/widgets/small_web_browser_overlay.dart index a8f3958c..c24aecd4 100644 --- a/apps/weblibre/lib/features/small_web/presentation/widgets/small_web_browser_overlay.dart +++ b/apps/weblibre/lib/features/small_web/presentation/widgets/small_web_browser_overlay.dart @@ -17,14 +17,18 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/logger.dart'; import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/small_web/presentation/controllers/small_web_mode_controller.dart'; import 'package:weblibre/features/small_web/presentation/controllers/small_web_session_controller.dart'; import 'package:weblibre/features/small_web/presentation/widgets/small_web_bottom_bar.dart'; import 'package:weblibre/features/small_web/presentation/widgets/small_web_menu_sheet.dart'; +import 'package:weblibre/utils/ui_helper.dart'; class SmallWebBrowserOverlay extends HookConsumerWidget { const SmallWebBrowserOverlay({super.key}); @@ -51,13 +55,17 @@ class SmallWebBrowserOverlay extends HookConsumerWidget { ); ref.listen(smallWebSessionControllerProvider, (prev, next) { - final error = next.asError?.error; + final asError = next.asError; + final error = asError?.error; final previousError = prev?.asError?.error; if (error != null && error != previousError && context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + logger.e( + 'Small web session error', + error: error, + stackTrace: asError?.stackTrace, + ); + showErrorMessage(context, 'Small web error: $error'); } }); @@ -71,7 +79,7 @@ class SmallWebBrowserOverlay extends HookConsumerWidget { currentTabTitle: tabState?.titleOrAuthority, onDiscover: () => ref.read(smallWebSessionControllerProvider.notifier).discover(), - onMenuTap: () => showSmallWebMenuSheet(context), + onMenuTap: () => unawaited(openSmallWebMenuFlow(context)), onExit: () => ref.read(smallWebModeControllerProvider.notifier).exit(), ), ); diff --git a/apps/weblibre/lib/features/small_web/presentation/widgets/small_web_menu_sheet.dart b/apps/weblibre/lib/features/small_web/presentation/widgets/small_web_menu_sheet.dart index 294a41b5..feceb49e 100644 --- a/apps/weblibre/lib/features/small_web/presentation/widgets/small_web_menu_sheet.dart +++ b/apps/weblibre/lib/features/small_web/presentation/widgets/small_web_menu_sheet.dart @@ -34,8 +34,29 @@ import 'package:weblibre/features/small_web/presentation/widgets/small_web_mode_ import 'package:weblibre/features/small_web/presentation/widgets/wander_console_sheet.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; -Future showSmallWebMenuSheet(BuildContext context) { - return showModalBottomSheet( +/// Hand-off between the small-web menu and the wander-console sheet. Each +/// sheet pops with one of these so [openSmallWebMenuFlow] can re-present the +/// next sheet from a stable parent context instead of the popped sheet's +/// deactivated context. +enum SmallWebSheetRequest { showMenu, showWander } + +/// Top-level entry: opens the small-web menu and re-presents whichever sheet +/// the user requested next, looping until a sheet is dismissed without a +/// follow-up request. +Future openSmallWebMenuFlow(BuildContext context) async { + SmallWebSheetRequest? next = SmallWebSheetRequest.showMenu; + while (next != null) { + if (!context.mounted) return; + if (next == SmallWebSheetRequest.showMenu) { + next = await showSmallWebMenuSheet(context); + } else { + next = await showWanderConsoleSheet(context); + } + } +} + +Future showSmallWebMenuSheet(BuildContext context) { + return showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( @@ -505,10 +526,8 @@ class _DefaultContentPanel extends ConsumerWidget { _WanderConsoleCard(currentConsoleUrl: session.currentConsoleUrl), const SizedBox(height: 8), FilledButton.tonalIcon( - onPressed: () async { - Navigator.of(context).pop(); - await showWanderConsoleSheet(context); - }, + onPressed: () => + Navigator.of(context).pop(SmallWebSheetRequest.showWander), icon: const Icon(Icons.dns, size: 18), label: const Text('Browse Consoles'), ), diff --git a/apps/weblibre/lib/features/small_web/presentation/widgets/wander_console_sheet.dart b/apps/weblibre/lib/features/small_web/presentation/widgets/wander_console_sheet.dart index 94c4bacd..6ab43924 100644 --- a/apps/weblibre/lib/features/small_web/presentation/widgets/wander_console_sheet.dart +++ b/apps/weblibre/lib/features/small_web/presentation/widgets/wander_console_sheet.dart @@ -29,9 +29,10 @@ import 'package:weblibre/presentation/widgets/failure_widget.dart'; import 'package:weblibre/presentation/widgets/sliding_pill_toggle.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; import 'package:weblibre/utils/form_validators.dart'; +import 'package:weblibre/utils/ui_helper.dart'; -Future showWanderConsoleSheet(BuildContext context) { - return showModalBottomSheet( +Future showWanderConsoleSheet(BuildContext context) { + return showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( @@ -78,10 +79,9 @@ class _WanderConsoleSheet extends HookConsumerWidget { child: Row( children: [ IconButton( - onPressed: () async { - Navigator.of(context).pop(); - await showSmallWebMenuSheet(context); - }, + onPressed: () => Navigator.of( + context, + ).pop(SmallWebSheetRequest.showMenu), icon: const Icon(Icons.arrow_back), style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -618,11 +618,10 @@ class _AddConsoleDialog extends HookWidget { if (!context.mounted) return; + // Show the snackbar first so it's anchored to a still-mounted context; + // popping the dialog afterwards deactivates this BuildContext. + showInfoMessage(context, 'Added console ${consoleUrl.host}'); Navigator.of(context).pop(); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Added console ${consoleUrl.host}')), - ); } on Exception catch (e) { if (!context.mounted) return; isLoading.value = false; diff --git a/apps/weblibre/lib/features/sync/presentation/screens/sync_settings.dart b/apps/weblibre/lib/features/sync/presentation/screens/sync_settings.dart index e05a6e1d..e722d971 100644 --- a/apps/weblibre/lib/features/sync/presentation/screens/sync_settings.dart +++ b/apps/weblibre/lib/features/sync/presentation/screens/sync_settings.dart @@ -25,6 +25,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; +import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/providers/format.dart'; import 'package:weblibre/features/qr_scanner/presentation/dialogs/qr_scanner_dialog.dart'; import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; @@ -454,23 +455,31 @@ class SyncSettingsScreen extends HookConsumerWidget { final newName = controller.text.trim(); if (newName.isEmpty) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Device name cannot be empty'), - ), + ui_helper.showErrorMessage( + context, + 'Device name cannot be empty', ); } return; } - final success = await onSave(newName).catchError((_) => false); + final success = await onSave(newName).catchError(( + Object error, + StackTrace stackTrace, + ) { + logger.e( + 'Failed to update device name', + error: error, + stackTrace: stackTrace, + ); + return false; + }); if (!success) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to update device name'), - ), + ui_helper.showErrorMessage( + context, + 'Failed to update device name', ); } return; @@ -518,10 +527,9 @@ class SyncSettingsScreen extends HookConsumerWidget { final uri = Uri.tryParse(value); if (uri == null || uri.scheme != 'https') { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Must be a valid HTTPS URL'), - ), + ui_helper.showErrorMessage( + context, + 'Must be a valid HTTPS URL', ); } return; diff --git a/apps/weblibre/lib/features/tor/domain/extensions/tor_status_x.dart b/apps/weblibre/lib/features/tor/domain/extensions/tor_status_x.dart new file mode 100644 index 00000000..adada571 --- /dev/null +++ b/apps/weblibre/lib/features/tor/domain/extensions/tor_status_x.dart @@ -0,0 +1,33 @@ +import 'package:flutter_tor/flutter_tor.dart'; +import 'package:riverpod/riverpod.dart'; + +enum TorPhase { stopped, bootstrapping, ready, degraded } + +extension TorStatusX on TorStatus { + TorPhase get phase { + if (!isRunning) { + return bootstrapProgress > 0 ? TorPhase.bootstrapping : TorPhase.stopped; + } + if (bootstrapProgress < 100) { + return TorPhase.bootstrapping; + } + return socksPort != null ? TorPhase.ready : TorPhase.degraded; + } + + bool get isReady => phase == TorPhase.ready; + bool get isBusy => phase == TorPhase.bootstrapping; + bool get isStopped => phase == TorPhase.stopped; + int? get usableSocksPort => isReady ? socksPort : null; +} + +extension TorAsyncStatusX on AsyncValue { + TorPhase get phase { + if (isLoading && !hasValue) return TorPhase.bootstrapping; + return value?.phase ?? TorPhase.stopped; + } + + bool get isReady => !isLoading && (value?.isReady ?? false); + bool get isBusy => isLoading || (value?.isBusy ?? false); + bool get isStopped => !isLoading && (value?.isStopped ?? true); + int? get usableSocksPort => isReady ? value?.socksPort : null; +} diff --git a/apps/weblibre/lib/features/tor/domain/repositories/tor_proxy.dart b/apps/weblibre/lib/features/tor/domain/repositories/tor_proxy.dart deleted file mode 100644 index 4302edfe..00000000 --- a/apps/weblibre/lib/features/tor/domain/repositories/tor_proxy.dart +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2024-2026 Fabian Freund. - * - * This file is part of WebLibre - * (see https://weblibre.eu). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import 'dart:async'; - -import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:synchronized/synchronized.dart'; -import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart'; - -part 'tor_proxy.g.dart'; - -@Riverpod(keepAlive: true) -class TorProxyRepository extends _$TorProxyRepository { - final _service = GeckoContainerProxyService(); - final _serviceLock = Lock(); - - Future setProxyPort(int port) { - return _serviceLock.synchronized(() async { - await _waitHealthcheck().timeout(const Duration(seconds: 30)); - - return _service.setProxyPort(port); - }); - } - - Future addContainerProxy(String contextId) { - return _serviceLock.synchronized(() async { - await _waitHealthcheck().timeout(const Duration(seconds: 10)); - - return _service.addContainerProxy(contextId); - }); - } - - Future removeContainerProxy(String contextId) { - return _serviceLock.synchronized(() async { - await _waitHealthcheck().timeout(const Duration(seconds: 10)); - - return _service.removeContainerProxy(contextId); - }); - } - - Future setSiteAssignments(List assignements) { - return _serviceLock.synchronized(() async { - await _waitHealthcheck().timeout(const Duration(seconds: 10)); - - return _service.setSiteAssignments( - Map.fromEntries( - assignements.map( - (e) => MapEntry( - e.assignedSite.origin, - e.contextualIdentity ?? 'general', - ), - ), - ), - ); - }); - } - - Future _waitHealthcheck({ - Duration timeout = const Duration(seconds: 15), - }) async { - final startTime = DateTime.now(); - - var healthy = await _service.healthcheck(); - while (!healthy) { - if (DateTime.now().difference(startTime) > timeout) { - throw TimeoutException('Timed out waiting for proxy service'); - } - - await Future.delayed(const Duration(milliseconds: 25)); - healthy = await _service.healthcheck(); - } - } - - @override - void build() { - return; - } -} diff --git a/apps/weblibre/lib/features/tor/domain/services/tor_proxy.dart b/apps/weblibre/lib/features/tor/domain/services/tor_proxy.dart index 0bbbbfcf..4f77b479 100644 --- a/apps/weblibre/lib/features/tor/domain/services/tor_proxy.dart +++ b/apps/weblibre/lib/features/tor/domain/services/tor_proxy.dart @@ -27,6 +27,7 @@ import 'package:rxdart/rxdart.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/features/tor/data/models/moat.dart'; import 'package:weblibre/features/tor/data/services/moat_service.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; import 'package:weblibre/features/tor/domain/repositories/builtin_bridges.dart'; import 'package:weblibre/features/user/data/models/tor_settings.dart'; import 'package:weblibre/features/user/domain/repositories/tor_settings.dart'; @@ -43,9 +44,7 @@ class TorProxyService extends _$TorProxyService { }) async { final currentStatus = await _tor.getStatus(); - if (!currentStatus.isRunning || - currentStatus.socksPort == null || - reconfigureIfRunning) { + if (!currentStatus.isReady || reconfigureIfRunning) { state = const AsyncLoading(); final torSettings = await ref @@ -164,7 +163,7 @@ class TorProxyService extends _$TorProxyService { } @override - Stream build() { + Stream build() async* { _statusSyncController = StreamController(); ref.onDispose(() async { @@ -172,6 +171,13 @@ class TorProxyService extends _$TorProxyService { await _tor.stop(); }); - return MergeStream([_tor.statusStream, _statusSyncController.stream]); + // Seed the provider with the current runtime state so screens that only + // watch the stream do not stay stuck in AsyncLoading until a manual sync. + yield await _tor.getStatus(); + yield* MergeStream([_tor.statusStream, _statusSyncController.stream]); } } + +Stream torLogStream(Ref ref) { + return ref.watch(torProxyServiceProvider.notifier)._tor.logStream; +} diff --git a/apps/weblibre/lib/features/tor/domain/services/tor_proxy.g.dart b/apps/weblibre/lib/features/tor/domain/services/tor_proxy.g.dart index 75b1b950..9ebb8f04 100644 --- a/apps/weblibre/lib/features/tor/domain/services/tor_proxy.g.dart +++ b/apps/weblibre/lib/features/tor/domain/services/tor_proxy.g.dart @@ -33,7 +33,7 @@ final class TorProxyServiceProvider TorProxyService create() => TorProxyService(); } -String _$torProxyServiceHash() => r'7b430ca32fbfc9ebebb1e52271c0183efdf60971'; +String _$torProxyServiceHash() => r'69240e5e8c5f6bb6aac1fd6c1c1215753607e34a'; abstract class _$TorProxyService extends $StreamNotifier { Stream build(); diff --git a/apps/weblibre/lib/features/tor/presentation/screens/tor_proxy.dart b/apps/weblibre/lib/features/tor/presentation/screens/tor_proxy.dart index 3d5df6ba..7f964fbe 100644 --- a/apps/weblibre/lib/features/tor/presentation/screens/tor_proxy.dart +++ b/apps/weblibre/lib/features/tor/presentation/screens/tor_proxy.dart @@ -23,67 +23,102 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nullability/nullability.dart'; -import 'package:weblibre/core/design/app_colors.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; +import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; import 'package:weblibre/features/tor/presentation/screens/country_picker.dart'; import 'package:weblibre/features/user/data/models/tor_settings.dart'; -import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/tor_settings.dart'; import 'package:weblibre/presentation/hooks/on_initialization.dart'; +import 'package:weblibre/presentation/icons/tor_icons.dart'; import 'package:weblibre/utils/ui_helper.dart'; +const List torProxySettingsSections = [ + SettingsSectionDefinition( + title: 'Service', + keywords: ['power', 'start', 'stop'], + entries: [ + SettingsEntryDefinition( + title: 'Tor™ Service', + subtitle: 'Start or stop the Tor service', + keywords: ['enable', 'connect'], + child: _TorServiceTile(), + ), + SettingsEntryDefinition( + title: 'Request New Identity', + subtitle: 'Use a fresh circuit for new connections', + keywords: ['circuit'], + child: _RequestNewIdentityTile(), + ), + ], + ), + SettingsSectionDefinition( + title: 'Circumvention', + keywords: ['bridges', 'transport', 'obfs4', 'snowflake'], + entries: [ + SettingsEntryDefinition( + title: 'Auto Configure Transport', + subtitle: + 'Pick the right pluggable transport for your network automatically', + keywords: ['auto'], + child: _AutoConfigureTransportTile(), + ), + SettingsEntryDefinition( + title: 'Transport', + subtitle: + 'Choose how to reach the Tor network when not auto-configured', + keywords: ['direct', 'obfs4', 'snowflake'], + child: _TransportSection(), + ), + ], + ), + SettingsSectionDefinition( + title: 'Country Restrictions', + keywords: ['entry', 'exit', 'country'], + entries: [ + SettingsEntryDefinition( + title: 'Entry Country', + subtitle: 'Choose the country of the entry guard', + keywords: ['guard'], + child: _CountryPickerTile(role: _NodeRole.entry), + ), + SettingsEntryDefinition( + title: 'Exit Country', + subtitle: 'Choose the country of the exit node', + keywords: ['exit'], + child: _CountryPickerTile(role: _NodeRole.exit), + ), + ], + ), + SettingsSectionDefinition( + title: 'About', + keywords: ['trademark', 'legal'], + entries: [ + SettingsEntryDefinition( + title: 'Trademark', + keywords: ['legal'], + child: ListTile( + leading: Icon(Icons.info_outline), + title: Text('Trademark'), + subtitle: Text( + 'Tor is a trademark of The Tor Project; all rights reserved. ' + 'WebLibre is not endorsed or sponsored by, or affiliated with, ' + 'the Tor Project.', + ), + isThreeLine: true, + ), + ), + ], + ), +]; + class TorProxyScreen extends HookConsumerWidget { const TorProxyScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final appColors = AppColors.of(context); - final bootstrapProgress = ref.watch( - torProxyServiceProvider.select( - (value) => value.value?.bootstrapProgress ?? 0, - ), - ); - - final torPendingRequest = useState(null); - ref.listen(torProxyServiceProvider, (previous, next) { - if (next.hasValue && torPendingRequest.value != null) { - if (next.requireValue.isRunning != previous?.value?.isRunning || - next.requireValue.bootstrapProgress != - previous?.value?.bootstrapProgress) { - if (torPendingRequest.value == true) { - if (next.requireValue.bootstrapProgress > 0) { - torPendingRequest.value = null; - } - } else { - torPendingRequest.value = null; - } - } - } - }); - - final torIsRunning = ref.watch( - torProxyServiceProvider.select( - (value) => value.value?.isRunning ?? false, - ), - ); - - final torIsBootstrapped = ref.watch( - torProxyServiceProvider.select( - (value) => value.value?.bootstrapProgress == 100, - ), - ); - - final torIsBusy = - torPendingRequest.value != null || - bootstrapProgress > 0 && bootstrapProgress < 100; - - final torSettings = ref.watch(torSettingsWithDefaultsProvider); - final showContainerUi = ref.watch( - generalSettingsWithDefaultsProvider.select((s) => s.showContainerUi), - ); - useOnInitialization(() async { await ref.read(torProxyServiceProvider.notifier).requestSync(); }); @@ -97,485 +132,294 @@ class TorProxyScreen extends HookConsumerWidget { } }); - return Scaffold( - body: Theme( - data: Theme.of(context).copyWith( - listTileTheme: ListTileTheme.of( - context, - ).copyWith(iconColor: Colors.white, textColor: Colors.white), - switchTheme: SwitchTheme.of(context).copyWith( - trackColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.isEmpty) { - return appColors.torBackgroundGrey; - } - return null; // Use the default color. - }), - trackOutlineColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.isEmpty) { - return Colors.white; - } - return null; // Use the default color. - }), - ), - radioTheme: RadioTheme.of(context).copyWith( - fillColor: WidgetStateColor.resolveWith((states) { - return Colors.white; - }), - ), - checkboxTheme: CheckboxTheme.of(context).copyWith( - fillColor: WidgetStateColor.resolveWith((states) { - return Colors.white; - }), - checkColor: WidgetStateProperty.all(appColors.torPurple), - ), - iconTheme: const IconThemeData(color: Colors.white), - ), - child: SafeArea( - child: ColoredBox( - color: appColors.torPurple, - child: CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - title: SwitchListTile.adaptive( - inactiveThumbColor: Colors.white, - activeThumbColor: appColors.torActiveGreen, - thumbIcon: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.contains(WidgetState.selected)) { - return const Icon(MdiIcons.axisArrowLock); - } - return null; // Use the default color. - }), - value: torPendingRequest.value ?? torIsRunning, - title: const Text('Tor™ Service'), - secondary: const Icon(MdiIcons.power), - onChanged: torIsBusy - ? null - : (value) async { - if (value) { - torPendingRequest.value = true; - - await ref - .read(torProxyServiceProvider.notifier) - .startOrReconfigure( - reconfigureIfRunning: false, - ); - } else { - torPendingRequest.value = false; - - await ref - .read(torProxyServiceProvider.notifier) - .disconnect(); - } - }, - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(4 + 40 + 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (torPendingRequest.value != false && torIsBusy) - LinearProgressIndicator( - backgroundColor: appColors.torBackgroundGrey, - color: appColors.torActiveGreen, - value: bootstrapProgress / 100, - ), - Padding( - padding: const EdgeInsets.only( - top: 4.0, - right: 16, - left: 16, - bottom: 4, - ), - child: SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: - torIsRunning && - torIsBootstrapped && - !torIsBusy - ? () async { - await ref - .read( - torProxyServiceProvider.notifier, - ) - .requestNewIdentity(); - - if (context.mounted) { - showInfoMessage( - context, - 'Requesting new Tor identity...', - ); - } - } - : null, - icon: const Icon(MdiIcons.refresh), - label: const Text('Request New Identity'), - ), - ), - ), - ], - ), - ), - ), - SliverList.list( - children: [ - const SizedBox(height: 16), - const Padding( - padding: EdgeInsets.only(left: 24.0), - child: Text( - 'Routing', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - RadioGroup( - groupValue: torSettings.proxyRegularTabsMode, - onChanged: (value) async { - if (value != null) { - await ref - .read(saveTorSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .proxyRegularTabsMode(value), - ); - } - }, - child: Column( - children: [ - if (showContainerUi) - const RadioListTile.adaptive( - value: TorRegularTabProxyMode.container, - title: Text('Container-Based Routing'), - subtitle: Text( - 'Route only tabs in Tor containers through the Tor network. Private tabs remain unaffected.', - ), - ), - const RadioListTile.adaptive( - value: TorRegularTabProxyMode.all, - title: Text('Global Routing'), - subtitle: Text( - 'Route all regular tabs through the Tor network. Private tabs remain unaffected.', - ), - ), - if (!showContainerUi && - torSettings.proxyRegularTabsMode == - TorRegularTabProxyMode.container) - const Padding( - padding: EdgeInsets.only( - left: 56, - right: 24, - top: 4, - ), - child: Text( - 'Container-based routing is currently active but hidden because Container UI is disabled.', - ), - ), - ], - ), - ), - - SwitchListTile.adaptive( - inactiveThumbColor: Colors.white, - activeThumbColor: appColors.torActiveGreen, - thumbIcon: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.contains(WidgetState.selected)) { - return const Icon(MdiIcons.incognito); - } - return null; // Use the default color. - }), - value: torSettings.proxyPrivateTabsTor, - title: const Text('Proxy Private Tabs'), - subtitle: const Text( - 'When enabled, all Private Tabs will be tunneled through Tor', - ), - secondary: const Icon(MdiIcons.incognito), - onChanged: (value) async { - await ref - .read(saveTorSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .proxyPrivateTabsTor(value), - ); - }, - ), - const SizedBox(height: 16), - const Padding( - padding: EdgeInsets.only(left: 24.0), - child: Text( - 'Circumvention', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - SwitchListTile.adaptive( - inactiveThumbColor: Colors.white, - activeThumbColor: appColors.torActiveGreen, - thumbIcon: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.contains(WidgetState.selected)) { - return const Icon(MdiIcons.arrowDecisionAuto); - } - return null; // Use the default color. - }), - value: torSettings.config == TorConnectionConfig.auto, - title: const Text('Auto Configure Transport'), - subtitle: const Text( - 'From some locations, it is necessary to use a pluggable transport to connect to Tor', - ), - secondary: const Icon(MdiIcons.arrowDecisionAuto), - onChanged: torIsBusy - ? null - : (value) async { - await ref - .read( - saveTorSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => - currentSettings.copyWith.config( - value - ? TorConnectionConfig.auto - : TorConnectionConfig.direct, - ), - ); - }, - ), - if (torSettings.config == TorConnectionConfig.auto) ...[ - SwitchListTile.adaptive( - inactiveThumbColor: Colors.white, - activeThumbColor: appColors.torActiveGreen, - value: torSettings.requireBridge, - contentPadding: const EdgeInsets.only( - left: 56, - right: 24, - ), - onChanged: torIsBusy - ? null - : (value) async { - await ref - .read( - saveTorSettingsControllerProvider - .notifier, - ) - .save( - (currentSettings) => currentSettings - .copyWith - .requireBridge(value), - ); - }, - title: const Text( - "I'm sure I cannot connect without a bridge", - ), - ), - ] else ...[ - RadioGroup( - groupValue: torSettings.config, - onChanged: (value) async { - if (value != null) { - await ref - .read( - saveTorSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => - currentSettings.copyWith.config(value), - ); - } - }, - child: Column( - children: [ - RadioListTile.adaptive( - value: TorConnectionConfig.direct, - enabled: !torIsBusy, - contentPadding: const EdgeInsets.only( - left: 56, - right: 24, - ), - title: const Text('Direct Connection'), - subtitle: const Text( - 'The best way to connect to Tor if Tor is not blocked', - ), - ), - RadioListTile.adaptive( - value: TorConnectionConfig.obfs4, - enabled: !torIsBusy, - contentPadding: const EdgeInsets.only( - left: 56, - right: 24, - ), - title: const Text('obfs4'), - subtitle: const Text( - 'Suitable for light censorship and high bandwidth needs', - ), - ), - RadioListTile.adaptive( - value: TorConnectionConfig.snowflake, - enabled: !torIsBusy, - contentPadding: const EdgeInsets.only( - left: 56, - right: 24, - ), - title: const Text('Snowflake'), - subtitle: const Text( - 'Suitable for heavy censorship', - ), - ), - ], - ), - ), - CheckboxListTile.adaptive( - value: torSettings.fetchRemoteBridges, - controlAffinity: ListTileControlAffinity.leading, - enabled: - torSettings.config != TorConnectionConfig.direct, - contentPadding: const EdgeInsets.only( - left: 56, - right: 24, - ), - onChanged: torIsBusy - ? null - : (value) async { - if (value != null) { - await ref - .read( - saveTorSettingsControllerProvider - .notifier, - ) - .save( - (currentSettings) => currentSettings - .copyWith - .fetchRemoteBridges(value), - ); - } - }, - title: const Text( - "Fetch fresh Bridges before connecting", - ), - ), - ], - const SizedBox(height: 16), - const Padding( - padding: EdgeInsets.only(left: 24.0), - child: Text( - 'Country Restrictions', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - ListTile( - enabled: !torIsBusy, - leading: - torSettings.entryNodeCountry.mapNotNull( - (code) => CountryFlag.fromCountryCode( - code, - theme: const EmojiTheme(size: 28), - ), - ) ?? - const Icon(Icons.public, color: Colors.white), - title: const Text('Entry Country'), - subtitle: Text( - torSettings.entryNodeCountry ?? 'Automatic', - ), - trailing: const Icon( - MdiIcons.chevronRight, - color: Colors.white, - ), - onTap: () async { - final result = await TorCountryPickerRoute( - title: 'Entry Country', - $extra: torSettings.entryNodeCountry, - ).push(context); - if (result == null) return; - final value = result == automaticCountry - ? null - : result; - await ref - .read(saveTorSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .entryNodeCountry(value), - ); - }, - ), - ListTile( - enabled: !torIsBusy, - leading: - torSettings.exitNodeCountry.mapNotNull( - (code) => CountryFlag.fromCountryCode( - code, - theme: const EmojiTheme(size: 28), - ), - ) ?? - const Icon(Icons.public, color: Colors.white), - title: const Text('Exit Country'), - subtitle: Text( - torSettings.exitNodeCountry ?? 'Automatic', - ), - trailing: const Icon( - MdiIcons.chevronRight, - color: Colors.white, - ), - onTap: () async { - final result = await TorCountryPickerRoute( - title: 'Exit Country', - $extra: torSettings.exitNodeCountry, - ).push(context); - if (result == null) return; - final value = result == automaticCountry - ? null - : result; - await ref - .read(saveTorSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .exitNodeCountry(value), - ); - }, - ), - ], - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only( - top: 32.0, - right: 12.0, - left: 12.0, - bottom: 8.0, - ), - child: Text( - 'Tor is a trademark of The Tor Project; all rights reserved. WebLibre is not endorsed or sponsored by, or affiliated with, the Tor Project.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontStyle: FontStyle.italic, - color: Colors.white, - ), - ), - ), - ), - ], - ), - ), - ), - ), + return const SettingsDetailScaffold( + title: 'Tor™ Proxy', + subtitle: + 'Onion routing, pluggable transports, bridges and country restrictions.', + icon: TorIcons.onionAlt, + sections: torProxySettingsSections, + ); + } +} + +class _TorServiceTile extends HookConsumerWidget { + const _TorServiceTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final status = ref.watch( + torProxyServiceProvider.select((value) => value.value), + ); + final pendingRequest = useState(null); + + ref.listen(torProxyServiceProvider, (previous, next) { + if (next.hasValue && pendingRequest.value != null) { + final current = next.requireValue; + final prev = previous?.value; + if (current.isRunning != prev?.isRunning || + current.bootstrapProgress != prev?.bootstrapProgress) { + if (pendingRequest.value == true) { + if (current.bootstrapProgress > 0) { + pendingRequest.value = null; + } + } else { + pendingRequest.value = null; + } + } + } + }); + + final isRunning = status?.isRunning ?? false; + final progress = status?.bootstrapProgress ?? 0; + final isBusy = pendingRequest.value != null || (status?.isBusy ?? false); + + return Column( + children: [ + SwitchListTile.adaptive( + secondary: const Icon(MdiIcons.power), + title: const Text('Tor™ Service'), + subtitle: const Text('Start or stop the Tor service'), + value: pendingRequest.value ?? isRunning, + onChanged: isBusy + ? null + : (value) async { + if (value) { + pendingRequest.value = true; + await ref + .read(torProxyServiceProvider.notifier) + .startOrReconfigure(reconfigureIfRunning: false); + } else { + pendingRequest.value = false; + await ref + .read(torProxyServiceProvider.notifier) + .disconnect(); + } + }, + ), + if (pendingRequest.value != false && isBusy) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: LinearProgressIndicator(value: progress / 100), + ), + ], + ); + } +} + +class _RequestNewIdentityTile extends ConsumerWidget { + const _RequestNewIdentityTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final status = ref.watch( + torProxyServiceProvider.select((value) => value.value), + ); + final enabled = status?.isReady ?? false; + + return ListTile( + enabled: enabled, + leading: const Icon(MdiIcons.refresh), + title: const Text('Request New Identity'), + subtitle: const Text('Use a fresh circuit for new connections'), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await ref.read(torProxyServiceProvider.notifier).requestNewIdentity(); + if (context.mounted) { + showInfoMessage(context, 'Requesting new Tor identity...'); + } + }, + ); + } +} + +class _AutoConfigureTransportTile extends ConsumerWidget { + const _AutoConfigureTransportTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final torSettings = ref.watch(torSettingsWithDefaultsProvider); + final isBusy = ref.watch( + torProxyServiceProvider.select((value) => value.isBusy), + ); + + return Column( + children: [ + SwitchListTile.adaptive( + secondary: const Icon(MdiIcons.arrowDecisionAuto), + title: const Text('Auto Configure Transport'), + subtitle: const Text( + 'From some locations, it is necessary to use a pluggable transport to connect to Tor', + ), + value: torSettings.config == TorConnectionConfig.auto, + onChanged: isBusy + ? null + : (value) async { + await ref + .read(saveTorSettingsControllerProvider.notifier) + .save( + (current) => current.copyWith.config( + value + ? TorConnectionConfig.auto + : TorConnectionConfig.direct, + ), + ); + }, + ), + if (torSettings.config == TorConnectionConfig.auto) + SwitchListTile.adaptive( + contentPadding: const EdgeInsets.only(left: 56, right: 24), + title: const Text("I'm sure I cannot connect without a bridge"), + value: torSettings.requireBridge, + onChanged: isBusy + ? null + : (value) async { + await ref + .read(saveTorSettingsControllerProvider.notifier) + .save( + (current) => current.copyWith.requireBridge(value), + ); + }, + ), + ], + ); + } +} + +class _TransportSection extends ConsumerWidget { + const _TransportSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final torSettings = ref.watch(torSettingsWithDefaultsProvider); + final isBusy = ref.watch( + torProxyServiceProvider.select((value) => value.isBusy), + ); + + if (torSettings.config == TorConnectionConfig.auto) { + return const ListTile( + leading: Icon(Icons.info_outline), + title: Text('Auto-configured'), + subtitle: Text( + 'Disable auto-configure above to pick a transport manually.', + ), + ); + } + + return Column( + children: [ + RadioGroup( + groupValue: torSettings.config, + onChanged: (value) async { + if (value != null) { + await ref + .read(saveTorSettingsControllerProvider.notifier) + .save((current) => current.copyWith.config(value)); + } + }, + child: Column( + children: [ + RadioListTile.adaptive( + value: TorConnectionConfig.direct, + enabled: !isBusy, + title: const Text('Direct Connection'), + subtitle: const Text( + 'The best way to connect to Tor if Tor is not blocked', + ), + ), + RadioListTile.adaptive( + value: TorConnectionConfig.obfs4, + enabled: !isBusy, + title: const Text('obfs4'), + subtitle: const Text( + 'Suitable for light censorship and high bandwidth needs', + ), + ), + RadioListTile.adaptive( + value: TorConnectionConfig.snowflake, + enabled: !isBusy, + title: const Text('Snowflake'), + subtitle: const Text('Suitable for heavy censorship'), + ), + ], + ), + ), + CheckboxListTile.adaptive( + controlAffinity: ListTileControlAffinity.leading, + enabled: !isBusy && torSettings.config != TorConnectionConfig.direct, + value: torSettings.fetchRemoteBridges, + title: const Text('Fetch fresh Bridges before connecting'), + onChanged: isBusy + ? null + : (value) async { + if (value != null) { + await ref + .read(saveTorSettingsControllerProvider.notifier) + .save( + (current) => + current.copyWith.fetchRemoteBridges(value), + ); + } + }, + ), + ], + ); + } +} + +enum _NodeRole { + entry(title: 'Entry Country'), + exit(title: 'Exit Country'); + + const _NodeRole({required this.title}); + + final String title; +} + +class _CountryPickerTile extends ConsumerWidget { + const _CountryPickerTile({required this.role}); + + final _NodeRole role; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final torSettings = ref.watch(torSettingsWithDefaultsProvider); + final isBusy = ref.watch( + torProxyServiceProvider.select((value) => value.isBusy), + ); + final country = switch (role) { + _NodeRole.entry => torSettings.entryNodeCountry, + _NodeRole.exit => torSettings.exitNodeCountry, + }; + + return ListTile( + enabled: !isBusy, + leading: + country.mapNotNull( + (code) => CountryFlag.fromCountryCode( + code, + theme: const EmojiTheme(size: 28), + ), + ) ?? + const Icon(Icons.public), + title: Text(role.title), + subtitle: Text(country ?? 'Automatic'), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final result = await TorCountryPickerRoute( + title: role.title, + $extra: country, + ).push(context); + if (result == null) return; + final value = result == automaticCountry ? null : result; + await ref + .read(saveTorSettingsControllerProvider.notifier) + .save( + (current) => switch (role) { + _NodeRole.entry => current.copyWith.entryNodeCountry(value), + _NodeRole.exit => current.copyWith.exitNodeCountry(value), + }, + ); + }, ); } } diff --git a/apps/weblibre/lib/features/tor/presentation/widgets/tor_notification.dart b/apps/weblibre/lib/features/tor/presentation/widgets/tor_notification.dart index c64af3af..de40dec9 100644 --- a/apps/weblibre/lib/features/tor/presentation/widgets/tor_notification.dart +++ b/apps/weblibre/lib/features/tor/presentation/widgets/tor_notification.dart @@ -21,6 +21,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/design/app_colors.dart'; import 'package:weblibre/features/geckoview/domain/controllers/overlay.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart'; import 'package:weblibre/presentation/widgets/animate_gradient_shader.dart'; @@ -33,10 +34,7 @@ class TorNotification extends HookConsumerWidget { final appColors = AppColors.of(context); ref.listen(torProxyServiceProvider, (previous, next) { - final status = next.value; - if (status != null && - status.isRunning && - status.bootstrapProgress >= 100) { + if (next.isReady) { ref.read(overlayControllerProvider.notifier).dismiss(); } }); diff --git a/apps/weblibre/lib/features/user/data/database/daos/proxy_profile.dart b/apps/weblibre/lib/features/user/data/database/daos/proxy_profile.dart new file mode 100644 index 00000000..cd81f86d --- /dev/null +++ b/apps/weblibre/lib/features/user/data/database/daos/proxy_profile.dart @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:drift/drift.dart'; +import 'package:weblibre/features/user/data/database/daos/proxy_profile.drift.dart'; +import 'package:weblibre/features/user/data/database/database.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart'; + +@DriftAccessor() +class ProxyProfileDao extends DatabaseAccessor + with $ProxyProfileDaoMixin { + ProxyProfileDao(super.attachedDatabase); + + Selectable watch() { + return db.proxyProfile.select() + ..orderBy([(t) => OrderingTerm.asc(t.createdAt)]); + } + + Future> fetchAll() => watch().get(); + + Future findById(String id) { + return (db.proxyProfile.select() + ..where((t) => t.id.equals(id)) + ..limit(1)) + .getSingleOrNull(); + } + + Future upsert(ProxyProfile profile) { + return db.proxyProfile.insertOne( + profile, + onConflict: DoUpdate( + (_) => ProxyProfileCompanion( + name: Value(profile.name), + type: Value(profile.type), + configJson: Value(profile.configJson), + dnsOverrideJson: Value(profile.dnsOverrideJson), + updatedAt: Value(profile.updatedAt), + ), + target: [db.proxyProfile.id], + ), + ); + } + + Future deleteById(String id) { + return (db.proxyProfile.delete()..where((t) => t.id.equals(id))).go(); + } +} diff --git a/apps/weblibre/lib/features/user/data/database/daos/proxy_profile.drift.dart b/apps/weblibre/lib/features/user/data/database/daos/proxy_profile.drift.dart new file mode 100644 index 00000000..a8a0f50a --- /dev/null +++ b/apps/weblibre/lib/features/user/data/database/daos/proxy_profile.drift.dart @@ -0,0 +1,13 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:weblibre/features/user/data/database/database.dart' as i1; + +mixin $ProxyProfileDaoMixin on i0.DatabaseAccessor { + ProxyProfileDaoManager get managers => ProxyProfileDaoManager(this); +} + +class ProxyProfileDaoManager { + final $ProxyProfileDaoMixin _db; + ProxyProfileDaoManager(this._db); +} diff --git a/apps/weblibre/lib/features/user/data/database/database.dart b/apps/weblibre/lib/features/user/data/database/database.dart index 85adb011..81d8009c 100644 --- a/apps/weblibre/lib/features/user/data/database/database.dart +++ b/apps/weblibre/lib/features/user/data/database/database.dart @@ -23,6 +23,7 @@ import 'package:drift_dev/api/migrations_native.dart'; import 'package:flutter/foundation.dart'; import 'package:weblibre/features/user/data/database/daos/cache.dart'; import 'package:weblibre/features/user/data/database/daos/onboarding.dart'; +import 'package:weblibre/features/user/data/database/daos/proxy_profile.dart'; import 'package:weblibre/features/user/data/database/daos/search_tokens.dart'; import 'package:weblibre/features/user/data/database/daos/setting.dart'; import 'package:weblibre/features/user/data/database/daos/toolbar_button_config.dart'; @@ -37,11 +38,12 @@ import 'package:weblibre/features/user/data/database/database.steps.dart'; OnboardingDao, ToolbarButtonConfigDao, SearchTokensDao, + ProxyProfileDao, ], ) class UserDatabase extends $UserDatabase { @override - final int schemaVersion = 5; + final int schemaVersion = 8; @override MigrationStrategy get migration => MigrationStrategy( @@ -103,5 +105,21 @@ class UserDatabase extends $UserDatabase { await m.addColumn(schema.searchTokens, schema.searchTokens.reservedAt); await m.createIndex(schema.idxSearchTokensReservedAt); }, + from5To6: (m, schema) async { + await m.createTable(schema.proxyProfile); + await m.createIndex(schema.idxProxyProfileUpdatedAt); + await m.createTable(schema.proxyRoutingSetting); + }, + from6To7: (m, schema) async { + await m.addColumn( + schema.proxyProfile, + schema.proxyProfile.dnsOverrideJson, + ); + }, + from7To8: (m, schema) async { + await m.database.customStatement( + 'DROP TABLE IF EXISTS proxy_routing_setting', + ); + }, ); } diff --git a/apps/weblibre/lib/features/user/data/database/database.drift.dart b/apps/weblibre/lib/features/user/data/database/database.drift.dart index 192d042d..80313043 100644 --- a/apps/weblibre/lib/features/user/data/database/database.drift.dart +++ b/apps/weblibre/lib/features/user/data/database/database.drift.dart @@ -12,13 +12,16 @@ import 'package:weblibre/features/user/data/database/daos/toolbar_button_config. as i6; import 'package:weblibre/features/user/data/database/daos/search_tokens.dart' as i7; -import 'package:drift/internal/modular.dart' as i8; -import 'package:sqlite3/common.dart' as i9; +import 'package:weblibre/features/user/data/database/daos/proxy_profile.dart' + as i8; +import 'package:drift/internal/modular.dart' as i9; +import 'package:sqlite3/common.dart' as i10; abstract class $UserDatabase extends i0.GeneratedDatabase { $UserDatabase(i0.QueryExecutor e) : super(e); $UserDatabaseManager get managers => $UserDatabaseManager(this); late final i1.Setting setting = i1.Setting(this); + late final i1.ProxyProfileTable proxyProfile = i1.ProxyProfileTable(this); late final i1.IconCache iconCache = i1.IconCache(this); late final i1.Onboarding onboarding = i1.Onboarding(this); late final i1.Riverpod riverpod = i1.Riverpod(this); @@ -35,7 +38,10 @@ abstract class $UserDatabase extends i0.GeneratedDatabase { late final i7.SearchTokensDao searchTokensDao = i7.SearchTokensDao( this as i3.UserDatabase, ); - i1.DefinitionsDrift get definitionsDrift => i8.ReadDatabaseContainer( + late final i8.ProxyProfileDao proxyProfileDao = i8.ProxyProfileDao( + this as i3.UserDatabase, + ); + i1.DefinitionsDrift get definitionsDrift => i9.ReadDatabaseContainer( this, ).accessor(i1.DefinitionsDrift.new); @override @@ -44,6 +50,8 @@ abstract class $UserDatabase extends i0.GeneratedDatabase { @override List get allSchemaEntities => [ setting, + proxyProfile, + i1.idxProxyProfileUpdatedAt, iconCache, onboarding, riverpod, @@ -60,6 +68,8 @@ class $UserDatabaseManager { $UserDatabaseManager(this._db); i1.$SettingTableManager get setting => i1.$SettingTableManager(_db, _db.setting); + i1.$ProxyProfileTableTableManager get proxyProfile => + i1.$ProxyProfileTableTableManager(_db, _db.proxyProfile); i1.$IconCacheTableManager get iconCache => i1.$IconCacheTableManager(_db, _db.iconCache); i1.$OnboardingTableManager get onboarding => @@ -72,7 +82,7 @@ class $UserDatabaseManager { i1.$SearchTokensTableManager(_db, _db.searchTokens); } -extension DefineFunctions on i9.CommonDatabase { +extension DefineFunctions on i10.CommonDatabase { void defineFunctions({ required String Function(int, String?) lexoRankNext, required String Function(int, String?) lexoRankPrevious, @@ -86,7 +96,7 @@ extension DefineFunctions on i9.CommonDatabase { }) { createFunction( functionName: 'lexo_rank_next', - argumentCount: const i9.AllowedArgumentCount(2), + argumentCount: const i10.AllowedArgumentCount(2), function: (args) { final arg0 = args[0] as int; final arg1 = args[1] as String?; @@ -95,7 +105,7 @@ extension DefineFunctions on i9.CommonDatabase { ); createFunction( functionName: 'lexo_rank_previous', - argumentCount: const i9.AllowedArgumentCount(2), + argumentCount: const i10.AllowedArgumentCount(2), function: (args) { final arg0 = args[0] as int; final arg1 = args[1] as String?; @@ -104,7 +114,7 @@ extension DefineFunctions on i9.CommonDatabase { ); createFunction( functionName: 'lexo_rank_reorder_after', - argumentCount: const i9.AllowedArgumentCount(2), + argumentCount: const i10.AllowedArgumentCount(2), function: (args) { final arg0 = args[0] as String?; final arg1 = args[1] as String?; @@ -113,7 +123,7 @@ extension DefineFunctions on i9.CommonDatabase { ); createFunction( functionName: 'lexo_rank_reorder_before', - argumentCount: const i9.AllowedArgumentCount(2), + argumentCount: const i10.AllowedArgumentCount(2), function: (args) { final arg0 = args[0] as String?; final arg1 = args[1] as String?; @@ -122,14 +132,14 @@ extension DefineFunctions on i9.CommonDatabase { ); createFunction( functionName: 'generate_content_hash', - argumentCount: const i9.AllowedArgumentCount(0), + argumentCount: const i10.AllowedArgumentCount(0), function: (args) { return generateContentHash(); }, ); createFunction( functionName: 'url_indexable', - argumentCount: const i9.AllowedArgumentCount(1), + argumentCount: const i10.AllowedArgumentCount(1), function: (args) { final arg0 = args[0] as String?; return urlIndexable(arg0); @@ -137,7 +147,7 @@ extension DefineFunctions on i9.CommonDatabase { ); createFunction( functionName: 'url_canonical', - argumentCount: const i9.AllowedArgumentCount(1), + argumentCount: const i10.AllowedArgumentCount(1), function: (args) { final arg0 = args[0] as String?; return urlCanonical(arg0); @@ -145,7 +155,7 @@ extension DefineFunctions on i9.CommonDatabase { ); createFunction( functionName: 'url_host', - argumentCount: const i9.AllowedArgumentCount(1), + argumentCount: const i10.AllowedArgumentCount(1), function: (args) { final arg0 = args[0] as String?; return urlHost(arg0); @@ -153,7 +163,7 @@ extension DefineFunctions on i9.CommonDatabase { ); createFunction( functionName: 'url_path', - argumentCount: const i9.AllowedArgumentCount(1), + argumentCount: const i10.AllowedArgumentCount(1), function: (args) { final arg0 = args[0] as String?; return urlPath(arg0); diff --git a/apps/weblibre/lib/features/user/data/database/database.steps.dart b/apps/weblibre/lib/features/user/data/database/database.steps.dart index 81532bf5..d70e8cca 100644 --- a/apps/weblibre/lib/features/user/data/database/database.steps.dart +++ b/apps/weblibre/lib/features/user/data/database/database.steps.dart @@ -563,11 +563,536 @@ i1.GeneratedColumn _column_19(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: '', ); + +final class Schema6 extends i0.VersionedSchema { + Schema6({required super.database}) : super(version: 6); + @override + late final List entities = [ + setting, + proxyProfile, + idxProxyProfileUpdatedAt, + proxyRoutingSetting, + iconCache, + onboarding, + riverpod, + toolbarButtonConfigs, + idxToolbarOrderKey, + searchTokens, + idxSearchTokensInsertedAt, + idxSearchTokensReservedAt, + ]; + late final Shape0 setting = Shape0( + source: i0.VersionedTable( + entityName: 'setting', + withoutRowId: false, + isStrict: true, + tableConstraints: [], + columns: [_column_0, _column_1, _column_2], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 proxyProfile = Shape7( + source: i0.VersionedTable( + entityName: 'proxy_profile', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_20, + _column_21, + _column_22, + _column_23, + _column_24, + _column_25, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxProxyProfileUpdatedAt = i1.Index( + 'idx_proxy_profile_updated_at', + 'CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)', + ); + late final Shape8 proxyRoutingSetting = Shape8( + source: i0.VersionedTable( + entityName: 'proxy_routing_setting', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_26, _column_27, _column_28, _column_29], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape1 iconCache = Shape1( + source: i0.VersionedTable( + entityName: 'icon_cache', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_3, _column_4, _column_5], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 onboarding = Shape2( + source: i0.VersionedTable( + entityName: 'onboarding', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_6, _column_7], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 riverpod = Shape3( + source: i0.VersionedTable( + entityName: 'riverpod', + withoutRowId: true, + isStrict: false, + tableConstraints: [], + columns: [_column_0, _column_8, _column_9, _column_10], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 toolbarButtonConfigs = Shape4( + source: i0.VersionedTable( + entityName: 'toolbar_button_configs', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_11, _column_12, _column_13, _column_14], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxToolbarOrderKey = i1.Index( + 'idx_toolbar_order_key', + 'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)', + ); + late final Shape6 searchTokens = Shape6( + source: i0.VersionedTable( + entityName: 'search_tokens', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_15, _column_16, _column_17, _column_18, _column_19], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxSearchTokensInsertedAt = i1.Index( + 'idx_search_tokens_inserted_at', + 'CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)', + ); + final i1.Index idxSearchTokensReservedAt = i1.Index( + 'idx_search_tokens_reserved_at', + 'CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)', + ); +} + +class Shape7 extends i0.VersionedTable { + Shape7({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get configJson => + columnsByName['config_json']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_20(String aliasedName) => + i1.GeneratedColumn( + 'id', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'NOT NULL PRIMARY KEY', + ); +i1.GeneratedColumn _column_21(String aliasedName) => + i1.GeneratedColumn( + 'name', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'NOT NULL', + ); +i1.GeneratedColumn _column_22(String aliasedName) => + i1.GeneratedColumn( + 'type', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'NOT NULL', + ); +i1.GeneratedColumn _column_23(String aliasedName) => + i1.GeneratedColumn( + 'config_json', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'NOT NULL', + ); +i1.GeneratedColumn _column_24(String aliasedName) => + i1.GeneratedColumn( + 'created_at', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const i1.CustomExpression('CURRENT_TIMESTAMP'), + ); +i1.GeneratedColumn _column_25(String aliasedName) => + i1.GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const i1.CustomExpression('CURRENT_TIMESTAMP'), + ); + +class Shape8 extends i0.VersionedTable { + Shape8({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get regularTabsMode => + columnsByName['regular_tabs_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get regularTabsProxyConnectionId => + columnsByName['regular_tabs_proxy_connection_id']! + as i1.GeneratedColumn; + i1.GeneratedColumn get privateTabsProxyConnectionId => + columnsByName['private_tabs_proxy_connection_id']! + as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_26(String aliasedName) => + i1.GeneratedColumn( + 'id', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL PRIMARY KEY CHECK (id = 1)', + ); +i1.GeneratedColumn _column_27(String aliasedName) => + i1.GeneratedColumn( + 'regular_tabs_mode', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'NOT NULL', + ); +i1.GeneratedColumn _column_28(String aliasedName) => + i1.GeneratedColumn( + 'regular_tabs_proxy_connection_id', + aliasedName, + true, + type: i1.DriftSqlType.string, + $customConstraints: '', + ); +i1.GeneratedColumn _column_29(String aliasedName) => + i1.GeneratedColumn( + 'private_tabs_proxy_connection_id', + aliasedName, + true, + type: i1.DriftSqlType.string, + $customConstraints: '', + ); + +final class Schema7 extends i0.VersionedSchema { + Schema7({required super.database}) : super(version: 7); + @override + late final List entities = [ + setting, + proxyProfile, + idxProxyProfileUpdatedAt, + proxyRoutingSetting, + iconCache, + onboarding, + riverpod, + toolbarButtonConfigs, + idxToolbarOrderKey, + searchTokens, + idxSearchTokensInsertedAt, + idxSearchTokensReservedAt, + ]; + late final Shape0 setting = Shape0( + source: i0.VersionedTable( + entityName: 'setting', + withoutRowId: false, + isStrict: true, + tableConstraints: [], + columns: [_column_0, _column_1, _column_2], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 proxyProfile = Shape9( + source: i0.VersionedTable( + entityName: 'proxy_profile', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_20, + _column_21, + _column_22, + _column_23, + _column_30, + _column_24, + _column_25, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxProxyProfileUpdatedAt = i1.Index( + 'idx_proxy_profile_updated_at', + 'CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)', + ); + late final Shape8 proxyRoutingSetting = Shape8( + source: i0.VersionedTable( + entityName: 'proxy_routing_setting', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_26, _column_27, _column_28, _column_29], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape1 iconCache = Shape1( + source: i0.VersionedTable( + entityName: 'icon_cache', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_3, _column_4, _column_5], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 onboarding = Shape2( + source: i0.VersionedTable( + entityName: 'onboarding', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_6, _column_7], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 riverpod = Shape3( + source: i0.VersionedTable( + entityName: 'riverpod', + withoutRowId: true, + isStrict: false, + tableConstraints: [], + columns: [_column_0, _column_8, _column_9, _column_10], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 toolbarButtonConfigs = Shape4( + source: i0.VersionedTable( + entityName: 'toolbar_button_configs', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_11, _column_12, _column_13, _column_14], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxToolbarOrderKey = i1.Index( + 'idx_toolbar_order_key', + 'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)', + ); + late final Shape6 searchTokens = Shape6( + source: i0.VersionedTable( + entityName: 'search_tokens', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_15, _column_16, _column_17, _column_18, _column_19], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxSearchTokensInsertedAt = i1.Index( + 'idx_search_tokens_inserted_at', + 'CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)', + ); + final i1.Index idxSearchTokensReservedAt = i1.Index( + 'idx_search_tokens_reserved_at', + 'CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)', + ); +} + +class Shape9 extends i0.VersionedTable { + Shape9({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get configJson => + columnsByName['config_json']! as i1.GeneratedColumn; + i1.GeneratedColumn get dnsOverrideJson => + columnsByName['dns_override_json']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_30(String aliasedName) => + i1.GeneratedColumn( + 'dns_override_json', + aliasedName, + true, + type: i1.DriftSqlType.string, + $customConstraints: '', + ); + +final class Schema8 extends i0.VersionedSchema { + Schema8({required super.database}) : super(version: 8); + @override + late final List entities = [ + setting, + proxyProfile, + idxProxyProfileUpdatedAt, + iconCache, + onboarding, + riverpod, + toolbarButtonConfigs, + idxToolbarOrderKey, + searchTokens, + idxSearchTokensInsertedAt, + idxSearchTokensReservedAt, + ]; + late final Shape0 setting = Shape0( + source: i0.VersionedTable( + entityName: 'setting', + withoutRowId: false, + isStrict: true, + tableConstraints: [], + columns: [_column_0, _column_1, _column_2], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 proxyProfile = Shape9( + source: i0.VersionedTable( + entityName: 'proxy_profile', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_20, + _column_21, + _column_22, + _column_23, + _column_30, + _column_24, + _column_25, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxProxyProfileUpdatedAt = i1.Index( + 'idx_proxy_profile_updated_at', + 'CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)', + ); + late final Shape1 iconCache = Shape1( + source: i0.VersionedTable( + entityName: 'icon_cache', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_3, _column_4, _column_5], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 onboarding = Shape2( + source: i0.VersionedTable( + entityName: 'onboarding', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_6, _column_7], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 riverpod = Shape3( + source: i0.VersionedTable( + entityName: 'riverpod', + withoutRowId: true, + isStrict: false, + tableConstraints: [], + columns: [_column_0, _column_8, _column_9, _column_10], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 toolbarButtonConfigs = Shape4( + source: i0.VersionedTable( + entityName: 'toolbar_button_configs', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_11, _column_12, _column_13, _column_14], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxToolbarOrderKey = i1.Index( + 'idx_toolbar_order_key', + 'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)', + ); + late final Shape6 searchTokens = Shape6( + source: i0.VersionedTable( + entityName: 'search_tokens', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_15, _column_16, _column_17, _column_18, _column_19], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxSearchTokensInsertedAt = i1.Index( + 'idx_search_tokens_inserted_at', + 'CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)', + ); + final i1.Index idxSearchTokensReservedAt = i1.Index( + 'idx_search_tokens_reserved_at', + 'CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)', + ); +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, required Future Function(i1.Migrator m, Schema4 schema) from3To4, required Future Function(i1.Migrator m, Schema5 schema) from4To5, + required Future Function(i1.Migrator m, Schema6 schema) from5To6, + required Future Function(i1.Migrator m, Schema7 schema) from6To7, + required Future Function(i1.Migrator m, Schema8 schema) from7To8, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -591,6 +1116,21 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from4To5(migrator, schema); return 5; + case 5: + final schema = Schema6(database: database); + final migrator = i1.Migrator(database, schema); + await from5To6(migrator, schema); + return 6; + case 6: + final schema = Schema7(database: database); + final migrator = i1.Migrator(database, schema); + await from6To7(migrator, schema); + return 7; + case 7: + final schema = Schema8(database: database); + final migrator = i1.Migrator(database, schema); + await from7To8(migrator, schema); + return 8; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -602,11 +1142,17 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema3 schema) from2To3, required Future Function(i1.Migrator m, Schema4 schema) from3To4, required Future Function(i1.Migrator m, Schema5 schema) from4To5, + required Future Function(i1.Migrator m, Schema6 schema) from5To6, + required Future Function(i1.Migrator m, Schema7 schema) from6To7, + required Future Function(i1.Migrator m, Schema8 schema) from7To8, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, from2To3: from2To3, from3To4: from3To4, from4To5: from4To5, + from5To6: from5To6, + from6To7: from6To7, + from7To8: from7To8, ), ); diff --git a/apps/weblibre/lib/features/user/data/database/definitions.drift b/apps/weblibre/lib/features/user/data/database/definitions.drift index 31ccefef..12badf24 100644 --- a/apps/weblibre/lib/features/user/data/database/definitions.drift +++ b/apps/weblibre/lib/features/user/data/database/definitions.drift @@ -1,9 +1,23 @@ +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; + CREATE TABLE setting ( "key" TEXT PRIMARY KEY NOT NULL, partition_key TEXT, "value" ANY ) STRICT; +CREATE TABLE proxy_profile ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + type ENUMNAME(SingboxProxyProfileType) NOT NULL, + config_json TEXT NOT NULL, + dns_override_json TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) AS ProxyProfile; + +CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile(updated_at); + CREATE TABLE icon_cache ( origin TEXT PRIMARY KEY NOT NULL, icon_data BLOB NOT NULL, @@ -104,4 +118,4 @@ evictCacheEntries: FROM icon_cache ORDER BY fetch_date DESC LIMIT -1 OFFSET :limit - ); \ No newline at end of file + ); diff --git a/apps/weblibre/lib/features/user/data/database/definitions.drift.dart b/apps/weblibre/lib/features/user/data/database/definitions.drift.dart index a379ec41..7f175c76 100644 --- a/apps/weblibre/lib/features/user/data/database/definitions.drift.dart +++ b/apps/weblibre/lib/features/user/data/database/definitions.drift.dart @@ -3,8 +3,9 @@ import 'package:drift/drift.dart' as i0; import 'package:weblibre/features/user/data/database/definitions.drift.dart' as i1; -import 'dart:typed_data' as i2; -import 'package:drift/internal/modular.dart' as i3; +import 'package:flutter_singbox_proxy/src/singbox_proxy_api.g.dart' as i2; +import 'dart:typed_data' as i3; +import 'package:drift/internal/modular.dart' as i4; typedef $SettingCreateCompanionBuilder = i1.SettingCompanion Function({ @@ -170,17 +171,276 @@ typedef $SettingProcessedTableManager = i1.SettingData, i0.PrefetchHooks Function() >; +typedef $ProxyProfileTableCreateCompanionBuilder = + i1.ProxyProfileCompanion Function({ + required String id, + required String name, + required i2.SingboxProxyProfileType type, + required String configJson, + i0.Value dnsOverrideJson, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value rowid, + }); +typedef $ProxyProfileTableUpdateCompanionBuilder = + i1.ProxyProfileCompanion Function({ + i0.Value id, + i0.Value name, + i0.Value type, + i0.Value configJson, + i0.Value dnsOverrideJson, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value rowid, + }); + +class $ProxyProfileTableFilterComposer + extends i0.Composer { + $ProxyProfileTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnWithTypeConverterFilters< + i2.SingboxProxyProfileType, + i2.SingboxProxyProfileType, + String + > + get type => $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); + + i0.ColumnFilters get configJson => $composableBuilder( + column: $table.configJson, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get dnsOverrideJson => $composableBuilder( + column: $table.dnsOverrideJson, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnFilters(column), + ); +} + +class $ProxyProfileTableOrderingComposer + extends i0.Composer { + $ProxyProfileTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get configJson => $composableBuilder( + column: $table.configJson, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get dnsOverrideJson => $composableBuilder( + column: $table.dnsOverrideJson, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column), + ); +} + +class $ProxyProfileTableAnnotationComposer + extends i0.Composer { + $ProxyProfileTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter + get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get configJson => $composableBuilder( + column: $table.configJson, + builder: (column) => column, + ); + + i0.GeneratedColumn get dnsOverrideJson => $composableBuilder( + column: $table.dnsOverrideJson, + builder: (column) => column, + ); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); +} + +class $ProxyProfileTableTableManager + extends + i0.RootTableManager< + i0.GeneratedDatabase, + i1.ProxyProfileTable, + i1.ProxyProfile, + i1.$ProxyProfileTableFilterComposer, + i1.$ProxyProfileTableOrderingComposer, + i1.$ProxyProfileTableAnnotationComposer, + $ProxyProfileTableCreateCompanionBuilder, + $ProxyProfileTableUpdateCompanionBuilder, + ( + i1.ProxyProfile, + i0.BaseReferences< + i0.GeneratedDatabase, + i1.ProxyProfileTable, + i1.ProxyProfile + >, + ), + i1.ProxyProfile, + i0.PrefetchHooks Function() + > { + $ProxyProfileTableTableManager( + i0.GeneratedDatabase db, + i1.ProxyProfileTable table, + ) : super( + i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$ProxyProfileTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$ProxyProfileTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$ProxyProfileTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value type = + const i0.Value.absent(), + i0.Value configJson = const i0.Value.absent(), + i0.Value dnsOverrideJson = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value rowid = const i0.Value.absent(), + }) => i1.ProxyProfileCompanion( + id: id, + name: name, + type: type, + configJson: configJson, + dnsOverrideJson: dnsOverrideJson, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required i2.SingboxProxyProfileType type, + required String configJson, + i0.Value dnsOverrideJson = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value rowid = const i0.Value.absent(), + }) => i1.ProxyProfileCompanion.insert( + id: id, + name: name, + type: type, + configJson: configJson, + dnsOverrideJson: dnsOverrideJson, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $ProxyProfileTableProcessedTableManager = + i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.ProxyProfileTable, + i1.ProxyProfile, + i1.$ProxyProfileTableFilterComposer, + i1.$ProxyProfileTableOrderingComposer, + i1.$ProxyProfileTableAnnotationComposer, + $ProxyProfileTableCreateCompanionBuilder, + $ProxyProfileTableUpdateCompanionBuilder, + ( + i1.ProxyProfile, + i0.BaseReferences< + i0.GeneratedDatabase, + i1.ProxyProfileTable, + i1.ProxyProfile + >, + ), + i1.ProxyProfile, + i0.PrefetchHooks Function() + >; typedef $IconCacheCreateCompanionBuilder = i1.IconCacheCompanion Function({ required String origin, - required i2.Uint8List iconData, + required i3.Uint8List iconData, required DateTime fetchDate, i0.Value rowid, }); typedef $IconCacheUpdateCompanionBuilder = i1.IconCacheCompanion Function({ i0.Value origin, - i0.Value iconData, + i0.Value iconData, i0.Value fetchDate, i0.Value rowid, }); @@ -199,7 +459,7 @@ class $IconCacheFilterComposer builder: (column) => i0.ColumnFilters(column), ); - i0.ColumnFilters get iconData => $composableBuilder( + i0.ColumnFilters get iconData => $composableBuilder( column: $table.iconData, builder: (column) => i0.ColumnFilters(column), ); @@ -224,7 +484,7 @@ class $IconCacheOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); - i0.ColumnOrderings get iconData => $composableBuilder( + i0.ColumnOrderings get iconData => $composableBuilder( column: $table.iconData, builder: (column) => i0.ColumnOrderings(column), ); @@ -247,7 +507,7 @@ class $IconCacheAnnotationComposer i0.GeneratedColumn get origin => $composableBuilder(column: $table.origin, builder: (column) => column); - i0.GeneratedColumn get iconData => + i0.GeneratedColumn get iconData => $composableBuilder(column: $table.iconData, builder: (column) => column); i0.GeneratedColumn get fetchDate => @@ -290,7 +550,7 @@ class $IconCacheTableManager updateCompanionCallback: ({ i0.Value origin = const i0.Value.absent(), - i0.Value iconData = const i0.Value.absent(), + i0.Value iconData = const i0.Value.absent(), i0.Value fetchDate = const i0.Value.absent(), i0.Value rowid = const i0.Value.absent(), }) => i1.IconCacheCompanion( @@ -302,7 +562,7 @@ class $IconCacheTableManager createCompanionCallback: ({ required String origin, - required i2.Uint8List iconData, + required i3.Uint8List iconData, required DateTime fetchDate, i0.Value rowid = const i0.Value.absent(), }) => i1.IconCacheCompanion.insert( @@ -866,7 +1126,7 @@ typedef $ToolbarButtonConfigsProcessedTableManager = typedef $SearchTokensCreateCompanionBuilder = i1.SearchTokensCompanion Function({ i0.Value id, - required i2.Uint8List token, + required i3.Uint8List token, required DateTime insertedAt, required String issuerKeyVersion, i0.Value reservedAt, @@ -874,7 +1134,7 @@ typedef $SearchTokensCreateCompanionBuilder = typedef $SearchTokensUpdateCompanionBuilder = i1.SearchTokensCompanion Function({ i0.Value id, - i0.Value token, + i0.Value token, i0.Value insertedAt, i0.Value issuerKeyVersion, i0.Value reservedAt, @@ -894,7 +1154,7 @@ class $SearchTokensFilterComposer builder: (column) => i0.ColumnFilters(column), ); - i0.ColumnFilters get token => $composableBuilder( + i0.ColumnFilters get token => $composableBuilder( column: $table.token, builder: (column) => i0.ColumnFilters(column), ); @@ -929,7 +1189,7 @@ class $SearchTokensOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); - i0.ColumnOrderings get token => $composableBuilder( + i0.ColumnOrderings get token => $composableBuilder( column: $table.token, builder: (column) => i0.ColumnOrderings(column), ); @@ -962,7 +1222,7 @@ class $SearchTokensAnnotationComposer i0.GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - i0.GeneratedColumn get token => + i0.GeneratedColumn get token => $composableBuilder(column: $table.token, builder: (column) => column); i0.GeneratedColumn get insertedAt => $composableBuilder( @@ -1017,7 +1277,7 @@ class $SearchTokensTableManager updateCompanionCallback: ({ i0.Value id = const i0.Value.absent(), - i0.Value token = const i0.Value.absent(), + i0.Value token = const i0.Value.absent(), i0.Value insertedAt = const i0.Value.absent(), i0.Value issuerKeyVersion = const i0.Value.absent(), i0.Value reservedAt = const i0.Value.absent(), @@ -1031,7 +1291,7 @@ class $SearchTokensTableManager createCompanionCallback: ({ i0.Value id = const i0.Value.absent(), - required i2.Uint8List token, + required i3.Uint8List token, required DateTime insertedAt, required String issuerKeyVersion, i0.Value reservedAt = const i0.Value.absent(), @@ -1297,6 +1557,418 @@ class SettingCompanion extends i0.UpdateCompanion { } } +class ProxyProfileTable extends i0.Table + with i0.TableInfo { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + ProxyProfileTable(this.attachedDatabase, [this._alias]); + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY', + ); + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final i0.GeneratedColumnWithTypeConverter< + i2.SingboxProxyProfileType, + String + > + type = + i0.GeneratedColumn( + 'type', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ).withConverter( + i1.ProxyProfileTable.$convertertype, + ); + late final i0.GeneratedColumn configJson = i0.GeneratedColumn( + 'config_json', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final i0.GeneratedColumn dnsOverrideJson = + i0.GeneratedColumn( + 'dns_override_json', + aliasedName, + true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn( + 'created_at', + aliasedName, + false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const i0.CustomExpression('CURRENT_TIMESTAMP'), + ); + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const i0.CustomExpression('CURRENT_TIMESTAMP'), + ); + @override + List get $columns => [ + id, + name, + type, + configJson, + dnsOverrideJson, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'proxy_profile'; + @override + Set get $primaryKey => {id}; + @override + i1.ProxyProfile map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.ProxyProfile( + id: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: i1.ProxyProfileTable.$convertertype.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + ), + configJson: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}config_json'], + )!, + dnsOverrideJson: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}dns_override_json'], + ), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + ProxyProfileTable createAlias(String alias) { + return ProxyProfileTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 + $convertertype = const i0.EnumNameConverter( + i2.SingboxProxyProfileType.values, + ); + @override + bool get dontWriteConstraints => true; +} + +class ProxyProfile extends i0.DataClass + implements i0.Insertable { + final String id; + final String name; + final i2.SingboxProxyProfileType type; + final String configJson; + final String? dnsOverrideJson; + final DateTime createdAt; + final DateTime updatedAt; + const ProxyProfile({ + required this.id, + required this.name, + required this.type, + required this.configJson, + this.dnsOverrideJson, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + { + map['type'] = i0.Variable( + i1.ProxyProfileTable.$convertertype.toSql(type), + ); + } + map['config_json'] = i0.Variable(configJson); + if (!nullToAbsent || dnsOverrideJson != null) { + map['dns_override_json'] = i0.Variable(dnsOverrideJson); + } + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + return map; + } + + factory ProxyProfile.fromJson( + Map json, { + i0.ValueSerializer? serializer, + }) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return ProxyProfile( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + type: i1.ProxyProfileTable.$convertertype.fromJson( + serializer.fromJson(json['type']), + ), + configJson: serializer.fromJson(json['config_json']), + dnsOverrideJson: serializer.fromJson(json['dns_override_json']), + createdAt: serializer.fromJson(json['created_at']), + updatedAt: serializer.fromJson(json['updated_at']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'type': serializer.toJson( + i1.ProxyProfileTable.$convertertype.toJson(type), + ), + 'config_json': serializer.toJson(configJson), + 'dns_override_json': serializer.toJson(dnsOverrideJson), + 'created_at': serializer.toJson(createdAt), + 'updated_at': serializer.toJson(updatedAt), + }; + } + + i1.ProxyProfile copyWith({ + String? id, + String? name, + i2.SingboxProxyProfileType? type, + String? configJson, + i0.Value dnsOverrideJson = const i0.Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + }) => i1.ProxyProfile( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + configJson: configJson ?? this.configJson, + dnsOverrideJson: dnsOverrideJson.present + ? dnsOverrideJson.value + : this.dnsOverrideJson, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + ProxyProfile copyWithCompanion(i1.ProxyProfileCompanion data) { + return ProxyProfile( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + configJson: data.configJson.present + ? data.configJson.value + : this.configJson, + dnsOverrideJson: data.dnsOverrideJson.present + ? data.dnsOverrideJson.value + : this.dnsOverrideJson, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ProxyProfile(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('configJson: $configJson, ') + ..write('dnsOverrideJson: $dnsOverrideJson, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + type, + configJson, + dnsOverrideJson, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.ProxyProfile && + other.id == this.id && + other.name == this.name && + other.type == this.type && + other.configJson == this.configJson && + other.dnsOverrideJson == this.dnsOverrideJson && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class ProxyProfileCompanion extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value type; + final i0.Value configJson; + final i0.Value dnsOverrideJson; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value rowid; + const ProxyProfileCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.configJson = const i0.Value.absent(), + this.dnsOverrideJson = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.rowid = const i0.Value.absent(), + }); + ProxyProfileCompanion.insert({ + required String id, + required String name, + required i2.SingboxProxyProfileType type, + required String configJson, + this.dnsOverrideJson = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.rowid = const i0.Value.absent(), + }) : id = i0.Value(id), + name = i0.Value(name), + type = i0.Value(type), + configJson = i0.Value(configJson); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? type, + i0.Expression? configJson, + i0.Expression? dnsOverrideJson, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? rowid, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (type != null) 'type': type, + if (configJson != null) 'config_json': configJson, + if (dnsOverrideJson != null) 'dns_override_json': dnsOverrideJson, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + i1.ProxyProfileCompanion copyWith({ + i0.Value? id, + i0.Value? name, + i0.Value? type, + i0.Value? configJson, + i0.Value? dnsOverrideJson, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? rowid, + }) { + return i1.ProxyProfileCompanion( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + configJson: configJson ?? this.configJson, + dnsOverrideJson: dnsOverrideJson ?? this.dnsOverrideJson, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.ProxyProfileTable.$convertertype.toSql(type.value), + ); + } + if (configJson.present) { + map['config_json'] = i0.Variable(configJson.value); + } + if (dnsOverrideJson.present) { + map['dns_override_json'] = i0.Variable(dnsOverrideJson.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = i0.Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProxyProfileCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('configJson: $configJson, ') + ..write('dnsOverrideJson: $dnsOverrideJson, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +i0.Index get idxProxyProfileUpdatedAt => i0.Index( + 'idx_proxy_profile_updated_at', + 'CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)', +); + class IconCache extends i0.Table with i0.TableInfo { @override @@ -1311,8 +1983,8 @@ class IconCache extends i0.Table requiredDuringInsert: true, $customConstraints: 'PRIMARY KEY NOT NULL', ); - late final i0.GeneratedColumn iconData = - i0.GeneratedColumn( + late final i0.GeneratedColumn iconData = + i0.GeneratedColumn( 'icon_data', aliasedName, false, @@ -1369,7 +2041,7 @@ class IconCache extends i0.Table class IconCacheData extends i0.DataClass implements i0.Insertable { final String origin; - final i2.Uint8List iconData; + final i3.Uint8List iconData; final DateTime fetchDate; const IconCacheData({ required this.origin, @@ -1380,7 +2052,7 @@ class IconCacheData extends i0.DataClass Map toColumns(bool nullToAbsent) { final map = {}; map['origin'] = i0.Variable(origin); - map['icon_data'] = i0.Variable(iconData); + map['icon_data'] = i0.Variable(iconData); map['fetch_date'] = i0.Variable(fetchDate); return map; } @@ -1392,7 +2064,7 @@ class IconCacheData extends i0.DataClass serializer ??= i0.driftRuntimeOptions.defaultSerializer; return IconCacheData( origin: serializer.fromJson(json['origin']), - iconData: serializer.fromJson(json['icon_data']), + iconData: serializer.fromJson(json['icon_data']), fetchDate: serializer.fromJson(json['fetch_date']), ); } @@ -1401,14 +2073,14 @@ class IconCacheData extends i0.DataClass serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { 'origin': serializer.toJson(origin), - 'icon_data': serializer.toJson(iconData), + 'icon_data': serializer.toJson(iconData), 'fetch_date': serializer.toJson(fetchDate), }; } i1.IconCacheData copyWith({ String? origin, - i2.Uint8List? iconData, + i3.Uint8List? iconData, DateTime? fetchDate, }) => i1.IconCacheData( origin: origin ?? this.origin, @@ -1447,7 +2119,7 @@ class IconCacheData extends i0.DataClass class IconCacheCompanion extends i0.UpdateCompanion { final i0.Value origin; - final i0.Value iconData; + final i0.Value iconData; final i0.Value fetchDate; final i0.Value rowid; const IconCacheCompanion({ @@ -1458,7 +2130,7 @@ class IconCacheCompanion extends i0.UpdateCompanion { }); IconCacheCompanion.insert({ required String origin, - required i2.Uint8List iconData, + required i3.Uint8List iconData, required DateTime fetchDate, this.rowid = const i0.Value.absent(), }) : origin = i0.Value(origin), @@ -1466,7 +2138,7 @@ class IconCacheCompanion extends i0.UpdateCompanion { fetchDate = i0.Value(fetchDate); static i0.Insertable custom({ i0.Expression? origin, - i0.Expression? iconData, + i0.Expression? iconData, i0.Expression? fetchDate, i0.Expression? rowid, }) { @@ -1480,7 +2152,7 @@ class IconCacheCompanion extends i0.UpdateCompanion { i1.IconCacheCompanion copyWith({ i0.Value? origin, - i0.Value? iconData, + i0.Value? iconData, i0.Value? fetchDate, i0.Value? rowid, }) { @@ -1499,7 +2171,7 @@ class IconCacheCompanion extends i0.UpdateCompanion { map['origin'] = i0.Variable(origin.value); } if (iconData.present) { - map['icon_data'] = i0.Variable(iconData.value); + map['icon_data'] = i0.Variable(iconData.value); } if (fetchDate.present) { map['fetch_date'] = i0.Variable(fetchDate.value); @@ -2245,8 +2917,8 @@ class SearchTokens extends i0.Table requiredDuringInsert: false, $customConstraints: 'PRIMARY KEY AUTOINCREMENT', ); - late final i0.GeneratedColumn token = - i0.GeneratedColumn( + late final i0.GeneratedColumn token = + i0.GeneratedColumn( 'token', aliasedName, false, @@ -2335,7 +3007,7 @@ class SearchTokens extends i0.Table class SearchToken extends i0.DataClass implements i0.Insertable { final int id; - final i2.Uint8List token; + final i3.Uint8List token; final DateTime insertedAt; final String issuerKeyVersion; final DateTime? reservedAt; @@ -2350,7 +3022,7 @@ class SearchToken extends i0.DataClass Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = i0.Variable(id); - map['token'] = i0.Variable(token); + map['token'] = i0.Variable(token); map['inserted_at'] = i0.Variable(insertedAt); map['issuer_key_version'] = i0.Variable(issuerKeyVersion); if (!nullToAbsent || reservedAt != null) { @@ -2366,7 +3038,7 @@ class SearchToken extends i0.DataClass serializer ??= i0.driftRuntimeOptions.defaultSerializer; return SearchToken( id: serializer.fromJson(json['id']), - token: serializer.fromJson(json['token']), + token: serializer.fromJson(json['token']), insertedAt: serializer.fromJson(json['inserted_at']), issuerKeyVersion: serializer.fromJson(json['issuer_key_version']), reservedAt: serializer.fromJson(json['reserved_at']), @@ -2377,7 +3049,7 @@ class SearchToken extends i0.DataClass serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'token': serializer.toJson(token), + 'token': serializer.toJson(token), 'inserted_at': serializer.toJson(insertedAt), 'issuer_key_version': serializer.toJson(issuerKeyVersion), 'reserved_at': serializer.toJson(reservedAt), @@ -2386,7 +3058,7 @@ class SearchToken extends i0.DataClass i1.SearchToken copyWith({ int? id, - i2.Uint8List? token, + i3.Uint8List? token, DateTime? insertedAt, String? issuerKeyVersion, i0.Value reservedAt = const i0.Value.absent(), @@ -2446,7 +3118,7 @@ class SearchToken extends i0.DataClass class SearchTokensCompanion extends i0.UpdateCompanion { final i0.Value id; - final i0.Value token; + final i0.Value token; final i0.Value insertedAt; final i0.Value issuerKeyVersion; final i0.Value reservedAt; @@ -2459,7 +3131,7 @@ class SearchTokensCompanion extends i0.UpdateCompanion { }); SearchTokensCompanion.insert({ this.id = const i0.Value.absent(), - required i2.Uint8List token, + required i3.Uint8List token, required DateTime insertedAt, required String issuerKeyVersion, this.reservedAt = const i0.Value.absent(), @@ -2468,7 +3140,7 @@ class SearchTokensCompanion extends i0.UpdateCompanion { issuerKeyVersion = i0.Value(issuerKeyVersion); static i0.Insertable custom({ i0.Expression? id, - i0.Expression? token, + i0.Expression? token, i0.Expression? insertedAt, i0.Expression? issuerKeyVersion, i0.Expression? reservedAt, @@ -2484,7 +3156,7 @@ class SearchTokensCompanion extends i0.UpdateCompanion { i1.SearchTokensCompanion copyWith({ i0.Value? id, - i0.Value? token, + i0.Value? token, i0.Value? insertedAt, i0.Value? issuerKeyVersion, i0.Value? reservedAt, @@ -2505,7 +3177,7 @@ class SearchTokensCompanion extends i0.UpdateCompanion { map['id'] = i0.Variable(id.value); } if (token.present) { - map['token'] = i0.Variable(token.value); + map['token'] = i0.Variable(token.value); } if (insertedAt.present) { map['inserted_at'] = i0.Variable(insertedAt.value); @@ -2541,7 +3213,7 @@ i0.Index get idxSearchTokensReservedAt => i0.Index( 'CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)', ); -class DefinitionsDrift extends i3.ModularAccessor { +class DefinitionsDrift extends i4.ModularAccessor { DefinitionsDrift(i0.GeneratedDatabase db) : super(db); i0.Selectable toolbarLeadingOrderKey({ required int bucket, @@ -2596,10 +3268,10 @@ class DefinitionsDrift extends i3.ModularAccessor { ); } - i1.ToolbarButtonConfigs get toolbarButtonConfigs => i3.ReadDatabaseContainer( + i1.ToolbarButtonConfigs get toolbarButtonConfigs => i4.ReadDatabaseContainer( attachedDatabase, ).resultSet('toolbar_button_configs'); - i1.IconCache get iconCache => i3.ReadDatabaseContainer( + i1.IconCache get iconCache => i4.ReadDatabaseContainer( attachedDatabase, ).resultSet('icon_cache'); } diff --git a/apps/weblibre/lib/features/user/data/models/proxy_dns_override.dart b/apps/weblibre/lib/features/user/data/models/proxy_dns_override.dart new file mode 100644 index 00000000..3aa74bd6 --- /dev/null +++ b/apps/weblibre/lib/features/user/data/models/proxy_dns_override.dart @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'proxy_dns_override.g.dart'; + +/// Mirrors sing-box's `dns.strategy` field. +enum ProxyDnsDomainStrategy { auto, preferIpv4, preferIpv6, ipv4Only, ipv6Only } + +extension ProxyDnsDomainStrategyExt on ProxyDnsDomainStrategy { + String get singboxValue => switch (this) { + ProxyDnsDomainStrategy.auto => '', + ProxyDnsDomainStrategy.preferIpv4 => 'prefer_ipv4', + ProxyDnsDomainStrategy.preferIpv6 => 'prefer_ipv6', + ProxyDnsDomainStrategy.ipv4Only => 'ipv4_only', + ProxyDnsDomainStrategy.ipv6Only => 'ipv6_only', + }; +} + +@JsonSerializable(includeIfNull: true) +class ProxyDnsOverride with FastEquatable { + /// Single DNS server resolved through this profile's own outbound. + final String? remoteServerAddress; + + final ProxyDnsDomainStrategy domainStrategy; + + ProxyDnsOverride({ + this.remoteServerAddress, + this.domainStrategy = ProxyDnsDomainStrategy.preferIpv4, + }); + + factory ProxyDnsOverride.fromJson(Map json) => + _$ProxyDnsOverrideFromJson(json); + + Map toJson() => _$ProxyDnsOverrideToJson(this); + + @override + List get hashParameters => [remoteServerAddress, domainStrategy]; +} diff --git a/apps/weblibre/lib/features/user/data/models/proxy_dns_override.g.dart b/apps/weblibre/lib/features/user/data/models/proxy_dns_override.g.dart new file mode 100644 index 00000000..5688e783 --- /dev/null +++ b/apps/weblibre/lib/features/user/data/models/proxy_dns_override.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'proxy_dns_override.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ProxyDnsOverride _$ProxyDnsOverrideFromJson(Map json) => + ProxyDnsOverride( + remoteServerAddress: json['remoteServerAddress'] as String?, + domainStrategy: + $enumDecodeNullable( + _$ProxyDnsDomainStrategyEnumMap, + json['domainStrategy'], + ) ?? + ProxyDnsDomainStrategy.preferIpv4, + ); + +Map _$ProxyDnsOverrideToJson( + ProxyDnsOverride instance, +) => { + 'remoteServerAddress': instance.remoteServerAddress, + 'domainStrategy': _$ProxyDnsDomainStrategyEnumMap[instance.domainStrategy]!, +}; + +const _$ProxyDnsDomainStrategyEnumMap = { + ProxyDnsDomainStrategy.auto: 'auto', + ProxyDnsDomainStrategy.preferIpv4: 'preferIpv4', + ProxyDnsDomainStrategy.preferIpv6: 'preferIpv6', + ProxyDnsDomainStrategy.ipv4Only: 'ipv4Only', + ProxyDnsDomainStrategy.ipv6Only: 'ipv6Only', +}; diff --git a/apps/weblibre/lib/features/user/data/models/proxy_routing_settings.dart b/apps/weblibre/lib/features/user/data/models/proxy_routing_settings.dart new file mode 100644 index 00000000..6b0a0102 --- /dev/null +++ b/apps/weblibre/lib/features/user/data/models/proxy_routing_settings.dart @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; + +part 'proxy_routing_settings.g.dart'; + +enum ProxyRegularTabRoutingMode { container, all } + +@CopyWith() +@JsonSerializable(includeIfNull: true, constructor: 'withDefaults') +class ProxyRoutingSettings with FastEquatable { + final ProxyRegularTabRoutingMode regularTabsMode; + + @JsonKey( + fromJson: _proxyConnectionIdFromJson, + toJson: _proxyConnectionIdToJson, + ) + final ProxyConnectionId? regularTabsProxyConnectionId; + + @JsonKey( + fromJson: _proxyConnectionIdFromJson, + toJson: _proxyConnectionIdToJson, + ) + final ProxyConnectionId? privateTabsProxyConnectionId; + + ProxyRoutingSettings({ + required this.regularTabsMode, + required this.regularTabsProxyConnectionId, + required this.privateTabsProxyConnectionId, + }); + + ProxyRoutingSettings.withDefaults({ + ProxyRegularTabRoutingMode? regularTabsMode, + this.regularTabsProxyConnectionId, + this.privateTabsProxyConnectionId, + }) : regularTabsMode = + regularTabsMode ?? ProxyRegularTabRoutingMode.container; + + factory ProxyRoutingSettings.fromJson(Map json) => + _$ProxyRoutingSettingsFromJson(json); + + Map toJson() => _$ProxyRoutingSettingsToJson(this); + + @override + List get hashParameters => [ + regularTabsMode, + regularTabsProxyConnectionId, + privateTabsProxyConnectionId, + ]; +} + +ProxyConnectionId? _proxyConnectionIdFromJson(String? json) => + ProxyConnectionId.decode(json); + +String? _proxyConnectionIdToJson(ProxyConnectionId? object) => object?.encode(); diff --git a/apps/weblibre/lib/features/user/data/models/proxy_routing_settings.g.dart b/apps/weblibre/lib/features/user/data/models/proxy_routing_settings.g.dart new file mode 100644 index 00000000..cec3e1eb --- /dev/null +++ b/apps/weblibre/lib/features/user/data/models/proxy_routing_settings.g.dart @@ -0,0 +1,136 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'proxy_routing_settings.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ProxyRoutingSettingsCWProxy { + ProxyRoutingSettings regularTabsMode( + ProxyRegularTabRoutingMode regularTabsMode, + ); + + ProxyRoutingSettings regularTabsProxyConnectionId( + ProxyConnectionId? regularTabsProxyConnectionId, + ); + + ProxyRoutingSettings privateTabsProxyConnectionId( + ProxyConnectionId? privateTabsProxyConnectionId, + ); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ProxyRoutingSettings(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// ProxyRoutingSettings(...).copyWith(id: 12, name: "My name") + /// ``` + ProxyRoutingSettings call({ + ProxyRegularTabRoutingMode regularTabsMode, + ProxyConnectionId? regularTabsProxyConnectionId, + ProxyConnectionId? privateTabsProxyConnectionId, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfProxyRoutingSettings.copyWith(...)` or call `instanceOfProxyRoutingSettings.copyWith.fieldName(value)` for a single field. +class _$ProxyRoutingSettingsCWProxyImpl + implements _$ProxyRoutingSettingsCWProxy { + const _$ProxyRoutingSettingsCWProxyImpl(this._value); + + final ProxyRoutingSettings _value; + + @override + ProxyRoutingSettings regularTabsMode( + ProxyRegularTabRoutingMode regularTabsMode, + ) => call(regularTabsMode: regularTabsMode); + + @override + ProxyRoutingSettings regularTabsProxyConnectionId( + ProxyConnectionId? regularTabsProxyConnectionId, + ) => call(regularTabsProxyConnectionId: regularTabsProxyConnectionId); + + @override + ProxyRoutingSettings privateTabsProxyConnectionId( + ProxyConnectionId? privateTabsProxyConnectionId, + ) => call(privateTabsProxyConnectionId: privateTabsProxyConnectionId); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ProxyRoutingSettings(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// ProxyRoutingSettings(...).copyWith(id: 12, name: "My name") + /// ``` + ProxyRoutingSettings call({ + Object? regularTabsMode = const $CopyWithPlaceholder(), + Object? regularTabsProxyConnectionId = const $CopyWithPlaceholder(), + Object? privateTabsProxyConnectionId = const $CopyWithPlaceholder(), + }) { + return ProxyRoutingSettings( + regularTabsMode: + regularTabsMode == const $CopyWithPlaceholder() || + regularTabsMode == null + ? _value.regularTabsMode + // ignore: cast_nullable_to_non_nullable + : regularTabsMode as ProxyRegularTabRoutingMode, + regularTabsProxyConnectionId: + regularTabsProxyConnectionId == const $CopyWithPlaceholder() + ? _value.regularTabsProxyConnectionId + // ignore: cast_nullable_to_non_nullable + : regularTabsProxyConnectionId as ProxyConnectionId?, + privateTabsProxyConnectionId: + privateTabsProxyConnectionId == const $CopyWithPlaceholder() + ? _value.privateTabsProxyConnectionId + // ignore: cast_nullable_to_non_nullable + : privateTabsProxyConnectionId as ProxyConnectionId?, + ); + } +} + +extension $ProxyRoutingSettingsCopyWith on ProxyRoutingSettings { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfProxyRoutingSettings.copyWith(...)` or `instanceOfProxyRoutingSettings.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ProxyRoutingSettingsCWProxy get copyWith => + _$ProxyRoutingSettingsCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ProxyRoutingSettings _$ProxyRoutingSettingsFromJson( + Map json, +) => ProxyRoutingSettings.withDefaults( + regularTabsMode: $enumDecodeNullable( + _$ProxyRegularTabRoutingModeEnumMap, + json['regularTabsMode'], + ), + regularTabsProxyConnectionId: _proxyConnectionIdFromJson( + json['regularTabsProxyConnectionId'] as String?, + ), + privateTabsProxyConnectionId: _proxyConnectionIdFromJson( + json['privateTabsProxyConnectionId'] as String?, + ), +); + +Map _$ProxyRoutingSettingsToJson( + ProxyRoutingSettings instance, +) => { + 'regularTabsMode': + _$ProxyRegularTabRoutingModeEnumMap[instance.regularTabsMode]!, + 'regularTabsProxyConnectionId': _proxyConnectionIdToJson( + instance.regularTabsProxyConnectionId, + ), + 'privateTabsProxyConnectionId': _proxyConnectionIdToJson( + instance.privateTabsProxyConnectionId, + ), +}; + +const _$ProxyRegularTabRoutingModeEnumMap = { + ProxyRegularTabRoutingMode.container: 'container', + ProxyRegularTabRoutingMode.all: 'all', +}; diff --git a/apps/weblibre/lib/features/user/data/models/tor_settings.dart b/apps/weblibre/lib/features/user/data/models/tor_settings.dart index 7879294a..62f3edd0 100644 --- a/apps/weblibre/lib/features/user/data/models/tor_settings.dart +++ b/apps/weblibre/lib/features/user/data/models/tor_settings.dart @@ -25,13 +25,9 @@ part 'tor_settings.g.dart'; enum TorConnectionConfig { auto, direct, obfs4, snowflake } -enum TorRegularTabProxyMode { container, all } - @CopyWith() @JsonSerializable(includeIfNull: true, constructor: 'withDefaults') class TorSettings with FastEquatable { - final TorRegularTabProxyMode proxyRegularTabsMode; - final bool proxyPrivateTabsTor; final TorConnectionConfig config; final bool requireBridge; final bool fetchRemoteBridges; @@ -39,8 +35,6 @@ class TorSettings with FastEquatable { final String? exitNodeCountry; TorSettings({ - required this.proxyRegularTabsMode, - required this.proxyPrivateTabsTor, required this.config, required this.requireBridge, required this.fetchRemoteBridges, @@ -49,17 +43,12 @@ class TorSettings with FastEquatable { }); TorSettings.withDefaults({ - TorRegularTabProxyMode? proxyRegularTabsMode, - bool? proxyPrivateTabsTor, TorConnectionConfig? config, bool? requireBridge, bool? fetchRemoteBridges, this.entryNodeCountry, this.exitNodeCountry, - }) : proxyRegularTabsMode = - proxyRegularTabsMode ?? TorRegularTabProxyMode.container, - proxyPrivateTabsTor = proxyPrivateTabsTor ?? false, - config = config ?? TorConnectionConfig.auto, + }) : config = config ?? TorConnectionConfig.auto, requireBridge = requireBridge ?? false, fetchRemoteBridges = fetchRemoteBridges ?? true; @@ -70,8 +59,6 @@ class TorSettings with FastEquatable { @override List get hashParameters => [ - proxyRegularTabsMode, - proxyPrivateTabsTor, config, requireBridge, fetchRemoteBridges, diff --git a/apps/weblibre/lib/features/user/data/models/tor_settings.g.dart b/apps/weblibre/lib/features/user/data/models/tor_settings.g.dart index dbc65f0a..3dc9b61c 100644 --- a/apps/weblibre/lib/features/user/data/models/tor_settings.g.dart +++ b/apps/weblibre/lib/features/user/data/models/tor_settings.g.dart @@ -7,10 +7,6 @@ part of 'tor_settings.dart'; // ************************************************************************** abstract class _$TorSettingsCWProxy { - TorSettings proxyRegularTabsMode(TorRegularTabProxyMode proxyRegularTabsMode); - - TorSettings proxyPrivateTabsTor(bool proxyPrivateTabsTor); - TorSettings config(TorConnectionConfig config); TorSettings requireBridge(bool requireBridge); @@ -29,8 +25,6 @@ abstract class _$TorSettingsCWProxy { /// TorSettings(...).copyWith(id: 12, name: "My name") /// ``` TorSettings call({ - TorRegularTabProxyMode proxyRegularTabsMode, - bool proxyPrivateTabsTor, TorConnectionConfig config, bool requireBridge, bool fetchRemoteBridges, @@ -46,15 +40,6 @@ class _$TorSettingsCWProxyImpl implements _$TorSettingsCWProxy { final TorSettings _value; - @override - TorSettings proxyRegularTabsMode( - TorRegularTabProxyMode proxyRegularTabsMode, - ) => call(proxyRegularTabsMode: proxyRegularTabsMode); - - @override - TorSettings proxyPrivateTabsTor(bool proxyPrivateTabsTor) => - call(proxyPrivateTabsTor: proxyPrivateTabsTor); - @override TorSettings config(TorConnectionConfig config) => call(config: config); @@ -83,8 +68,6 @@ class _$TorSettingsCWProxyImpl implements _$TorSettingsCWProxy { /// TorSettings(...).copyWith(id: 12, name: "My name") /// ``` TorSettings call({ - Object? proxyRegularTabsMode = const $CopyWithPlaceholder(), - Object? proxyPrivateTabsTor = const $CopyWithPlaceholder(), Object? config = const $CopyWithPlaceholder(), Object? requireBridge = const $CopyWithPlaceholder(), Object? fetchRemoteBridges = const $CopyWithPlaceholder(), @@ -92,18 +75,6 @@ class _$TorSettingsCWProxyImpl implements _$TorSettingsCWProxy { Object? exitNodeCountry = const $CopyWithPlaceholder(), }) { return TorSettings( - proxyRegularTabsMode: - proxyRegularTabsMode == const $CopyWithPlaceholder() || - proxyRegularTabsMode == null - ? _value.proxyRegularTabsMode - // ignore: cast_nullable_to_non_nullable - : proxyRegularTabsMode as TorRegularTabProxyMode, - proxyPrivateTabsTor: - proxyPrivateTabsTor == const $CopyWithPlaceholder() || - proxyPrivateTabsTor == null - ? _value.proxyPrivateTabsTor - // ignore: cast_nullable_to_non_nullable - : proxyPrivateTabsTor as bool, config: config == const $CopyWithPlaceholder() || config == null ? _value.config // ignore: cast_nullable_to_non_nullable @@ -144,11 +115,6 @@ extension $TorSettingsCopyWith on TorSettings { TorSettings _$TorSettingsFromJson(Map json) => TorSettings.withDefaults( - proxyRegularTabsMode: $enumDecodeNullable( - _$TorRegularTabProxyModeEnumMap, - json['proxyRegularTabsMode'], - ), - proxyPrivateTabsTor: json['proxyPrivateTabsTor'] as bool?, config: $enumDecodeNullable(_$TorConnectionConfigEnumMap, json['config']), requireBridge: json['requireBridge'] as bool?, fetchRemoteBridges: json['fetchRemoteBridges'] as bool?, @@ -158,9 +124,6 @@ TorSettings _$TorSettingsFromJson(Map json) => Map _$TorSettingsToJson(TorSettings instance) => { - 'proxyRegularTabsMode': - _$TorRegularTabProxyModeEnumMap[instance.proxyRegularTabsMode]!, - 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, 'config': _$TorConnectionConfigEnumMap[instance.config]!, 'requireBridge': instance.requireBridge, 'fetchRemoteBridges': instance.fetchRemoteBridges, @@ -168,11 +131,6 @@ Map _$TorSettingsToJson(TorSettings instance) => 'exitNodeCountry': instance.exitNodeCountry, }; -const _$TorRegularTabProxyModeEnumMap = { - TorRegularTabProxyMode.container: 'container', - TorRegularTabProxyMode.all: 'all', -}; - const _$TorConnectionConfigEnumMap = { TorConnectionConfig.auto: 'auto', TorConnectionConfig.direct: 'direct', diff --git a/apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup.dart b/apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup.dart index f853eac4..ddf53b77 100644 --- a/apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup.dart +++ b/apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup.dart @@ -49,13 +49,20 @@ class ProfileBackupScreen extends HookConsumerWidget { final backupFuture = useState?>(null); final backupState = useFuture(backupFuture.value); + // One-shot: success navigates away; rebuilds before the route swap + // completes would otherwise re-fire navigation and the snackbar. + final successHandled = useRef(false); + useEffect(() { if (backupState.hasError) { WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; showErrorMessage(context, backupState.error!.toString()); }); - } else if (backupState.hasData) { + } else if (backupState.hasData && !successHandled.value) { + successHandled.value = true; WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; showInfoMessage(context, 'Backup created successfully'); ProfileListRoute().go(context); }); diff --git a/apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart b/apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart index 7407c0e8..93a26aad 100644 --- a/apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart +++ b/apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart @@ -55,13 +55,20 @@ class ProfileRestoreScreen extends HookConsumerWidget { final restoreTarget = useState(forcedTarget ?? RestoreTarget.createNew); + // One-shot: a successful restore navigates away, but rebuilds before the + // route swap completes can otherwise re-fire the success branch. + final successHandled = useRef(false); + useEffect(() { if (restoreState.hasError) { WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; showErrorMessage(context, restoreState.error!.toString()); }); - } else if (restoreState.hasData) { + } else if (restoreState.hasData && !successHandled.value) { + successHandled.value = true; WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; showInfoMessage(context, 'Backup restored successfully'); if (onRestoreSuccess != null) { onRestoreSuccess!(context, restoreState.data); diff --git a/apps/weblibre/lib/features/user/domain/repositories/proxy_routing_settings.dart b/apps/weblibre/lib/features/user/domain/repositories/proxy_routing_settings.dart new file mode 100644 index 00000000..81f25a14 --- /dev/null +++ b/apps/weblibre/lib/features/user/domain/repositories/proxy_routing_settings.dart @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:drift/drift.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart'; +import 'package:weblibre/features/user/data/providers.dart'; + +part 'proxy_routing_settings.g.dart'; + +typedef UpdateProxyRoutingSettingsFunc = + ProxyRoutingSettings Function(ProxyRoutingSettings currentSettings); + +@Riverpod(keepAlive: true) +class ProxyRoutingSettingsRepository extends _$ProxyRoutingSettingsRepository { + final _partitionKey = 'proxy_routing'; + + ProxyRoutingSettings _deserializeSettings( + List> entries, + ) { + final settings = Map.fromEntries(entries); + + final db = ref.read(userDatabaseProvider); + + return ProxyRoutingSettings.fromJson({ + 'regularTabsMode': settings['regularTabsMode']?.readAs( + DriftSqlType.string, + db.typeMapping, + ), + 'regularTabsProxyConnectionId': settings['regularTabsProxyConnectionId'] + ?.readAs(DriftSqlType.string, db.typeMapping), + 'privateTabsProxyConnectionId': settings['privateTabsProxyConnectionId'] + ?.readAs(DriftSqlType.string, db.typeMapping), + }); + } + + //Eager fetch, when up to date settings are required + Future fetchSettings() { + return ref + .read(userDatabaseProvider) + .settingDao + .getAllSettingsOfPartitionKey(_partitionKey) + .get() + .then(_deserializeSettings); + } + + Future updateSettings( + UpdateProxyRoutingSettingsFunc updateWithCurrent, + ) async { + final db = ref.read(userDatabaseProvider); + + final current = await fetchSettings(); + + final oldJson = current.toJson(); + final newJson = updateWithCurrent(current).toJson(); + + return db.transaction(() async { + for (final MapEntry(:key, :value) in newJson.entries) { + if (oldJson[key] != value) { + await db.settingDao.updateSetting(key, _partitionKey, value); + } + } + }); + } + + @override + Stream build() { + final db = ref.watch(userDatabaseProvider); + + return db.settingDao + .getAllSettingsOfPartitionKey(_partitionKey) + .watch() + .map(_deserializeSettings); + } +} + +@Riverpod(keepAlive: true) +ProxyRoutingSettings proxyRoutingSettingsWithDefaults(Ref ref) { + return ref.watch( + proxyRoutingSettingsRepositoryProvider.select( + (value) => value.value ?? ProxyRoutingSettings.withDefaults(), + ), + ); +} diff --git a/apps/weblibre/lib/features/user/domain/repositories/proxy_routing_settings.g.dart b/apps/weblibre/lib/features/user/domain/repositories/proxy_routing_settings.g.dart new file mode 100644 index 00000000..7fec48e7 --- /dev/null +++ b/apps/weblibre/lib/features/user/domain/repositories/proxy_routing_settings.g.dart @@ -0,0 +1,115 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'proxy_routing_settings.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ProxyRoutingSettingsRepository) +final proxyRoutingSettingsRepositoryProvider = + ProxyRoutingSettingsRepositoryProvider._(); + +final class ProxyRoutingSettingsRepositoryProvider + extends + $StreamNotifierProvider< + ProxyRoutingSettingsRepository, + ProxyRoutingSettings + > { + ProxyRoutingSettingsRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'proxyRoutingSettingsRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$proxyRoutingSettingsRepositoryHash(); + + @$internal + @override + ProxyRoutingSettingsRepository create() => ProxyRoutingSettingsRepository(); +} + +String _$proxyRoutingSettingsRepositoryHash() => + r'10c46172bad7426bb639cf3bc769ba150bdb30c3'; + +abstract class _$ProxyRoutingSettingsRepository + extends $StreamNotifier { + Stream build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref, ProxyRoutingSettings>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue, + ProxyRoutingSettings + >, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + +@ProviderFor(proxyRoutingSettingsWithDefaults) +final proxyRoutingSettingsWithDefaultsProvider = + ProxyRoutingSettingsWithDefaultsProvider._(); + +final class ProxyRoutingSettingsWithDefaultsProvider + extends + $FunctionalProvider< + ProxyRoutingSettings, + ProxyRoutingSettings, + ProxyRoutingSettings + > + with $Provider { + ProxyRoutingSettingsWithDefaultsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'proxyRoutingSettingsWithDefaultsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$proxyRoutingSettingsWithDefaultsHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + ProxyRoutingSettings create(Ref ref) { + return proxyRoutingSettingsWithDefaults(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ProxyRoutingSettings value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$proxyRoutingSettingsWithDefaultsHash() => + r'899f545188633843d5224bf86d4611d80543cf5c'; diff --git a/apps/weblibre/lib/features/user/domain/repositories/tor_settings.dart b/apps/weblibre/lib/features/user/domain/repositories/tor_settings.dart index c1234cc3..1dac40df 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/tor_settings.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/tor_settings.dart @@ -38,14 +38,6 @@ class TorSettingsRepository extends _$TorSettingsRepository { final db = ref.read(userDatabaseProvider); return TorSettings.fromJson({ - 'proxyRegularTabsMode': settings['proxyRegularTabsMode']?.readAs( - DriftSqlType.string, - db.typeMapping, - ), - 'proxyPrivateTabsTor': settings['proxyPrivateTabsTor']?.readAs( - DriftSqlType.bool, - db.typeMapping, - ), 'config': settings['config']?.readAs(DriftSqlType.string, db.typeMapping), 'requireBridge': settings['requireBridge']?.readAs( DriftSqlType.bool, diff --git a/apps/weblibre/lib/features/user/domain/repositories/tor_settings.g.dart b/apps/weblibre/lib/features/user/domain/repositories/tor_settings.g.dart index fede70fb..029c689a 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/tor_settings.g.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/tor_settings.g.dart @@ -34,7 +34,7 @@ final class TorSettingsRepositoryProvider } String _$torSettingsRepositoryHash() => - r'f771f23f17903bd192b24604bab522fb20571ffd'; + r'10fa5ad15138e3df24940c3462d62a9df9fe5eef'; abstract class _$TorSettingsRepository extends $StreamNotifier { Stream build(); diff --git a/apps/weblibre/lib/features/web_search/domain/controllers/sandbox_capture_controller.dart b/apps/weblibre/lib/features/web_search/domain/controllers/sandbox_capture_controller.dart index fb703653..9fc23607 100644 --- a/apps/weblibre/lib/features/web_search/domain/controllers/sandbox_capture_controller.dart +++ b/apps/weblibre/lib/features/web_search/domain/controllers/sandbox_capture_controller.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart' as fmc; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:search_client/search_client.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; diff --git a/apps/weblibre/lib/features/web_search/domain/controllers/search_controller.dart b/apps/weblibre/lib/features/web_search/domain/controllers/search_controller.dart index 9417fa53..657bea3f 100644 --- a/apps/weblibre/lib/features/web_search/domain/controllers/search_controller.dart +++ b/apps/weblibre/lib/features/web_search/domain/controllers/search_controller.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:search_client/search_client.dart'; import 'package:weblibre/domain/services/generic_website.dart'; import 'package:weblibre/features/search_credits/domain/controllers/search_token_issuance_controller.dart'; @@ -12,6 +12,7 @@ import 'package:weblibre/features/search_credits/domain/providers.dart'; import 'package:weblibre/features/search_credits/domain/providers/proxy_client.dart'; import 'package:weblibre/features/search_credits/domain/repositories/search_token_stash_repository.dart'; import 'package:weblibre/features/search_credits/domain/repositories/web_search_settings.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; import 'package:weblibre/features/web_search/domain/entities/captured_page_state.dart'; @@ -687,10 +688,7 @@ class MetaSearchController extends _$MetaSearchController { final completer = Completer(); final subscription = ref.listen(torProxyServiceProvider, (previous, next) { - final status = next.value; - if (status != null && - status.isRunning && - status.bootstrapProgress >= 100) { + if (next.isReady) { if (!completer.isCompleted) completer.complete(true); } }); diff --git a/apps/weblibre/lib/features/web_search/domain/controllers/search_controller.g.dart b/apps/weblibre/lib/features/web_search/domain/controllers/search_controller.g.dart index ec51c859..34d17ea2 100644 --- a/apps/weblibre/lib/features/web_search/domain/controllers/search_controller.g.dart +++ b/apps/weblibre/lib/features/web_search/domain/controllers/search_controller.g.dart @@ -42,7 +42,7 @@ final class MetaSearchControllerProvider } String _$metaSearchControllerHash() => - r'b040527dfb49d9407fe0ea22169f5961d1faef13'; + r'afb6600f78717df124a6afdb80710eba74666d23'; abstract class _$MetaSearchController extends $Notifier { MetaSearchState build(); diff --git a/apps/weblibre/lib/features/web_search/domain/services/capture_artifact_downloader.dart b/apps/weblibre/lib/features/web_search/domain/services/capture_artifact_downloader.dart index b9b4d68e..8571801d 100644 --- a/apps/weblibre/lib/features/web_search/domain/services/capture_artifact_downloader.dart +++ b/apps/weblibre/lib/features/web_search/domain/services/capture_artifact_downloader.dart @@ -5,7 +5,7 @@ import 'dart:ui'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:search_client/search_client.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/features/search_credits/domain/providers.dart'; diff --git a/apps/weblibre/lib/features/web_search/presentation/dialogs/fetch_method_dialog.dart b/apps/weblibre/lib/features/web_search/presentation/dialogs/fetch_method_dialog.dart index eaccc325..8f4abb2c 100644 --- a/apps/weblibre/lib/features/web_search/presentation/dialogs/fetch_method_dialog.dart +++ b/apps/weblibre/lib/features/web_search/presentation/dialogs/fetch_method_dialog.dart @@ -125,7 +125,9 @@ class _MethodTile extends ConsumerWidget { failed: failed, colorScheme: colorScheme, ), - onTap: busy ? null : () => _handleTap(context, ref, ready, failed, captured), + onTap: busy + ? null + : () => _handleTap(context, ref, ready, failed, captured), ); } @@ -200,9 +202,6 @@ class _StatusIndicator extends StatelessWidget { if (failed) { return Icon(Icons.refresh, color: colorScheme.error); } - return Icon( - Icons.download_rounded, - color: colorScheme.onSurfaceVariant, - ); + return Icon(Icons.download_rounded, color: colorScheme.onSurfaceVariant); } } diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/page_metadata_chips.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/page_metadata_chips.dart index faeb5e1c..53698b15 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/page_metadata_chips.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/page_metadata_chips.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:intl/intl.dart'; import 'package:nullability/nullability.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; class PageMetadataChips extends HookWidget { final PageMetadata metadata; diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/route_through_tor_toggle.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/route_through_tor_toggle.dart index dccb8a55..56fb5d0b 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/route_through_tor_toggle.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/route_through_tor_toggle.dart @@ -22,6 +22,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/design/app_colors.dart'; import 'package:weblibre/features/search_credits/domain/providers/proxy_client.dart'; import 'package:weblibre/features/search_credits/domain/repositories/web_search_settings.dart'; +import 'package:weblibre/features/tor/domain/extensions/tor_status_x.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; import 'package:weblibre/presentation/hooks/on_initialization.dart'; @@ -55,10 +56,9 @@ class RouteThroughTorToggle extends HookConsumerWidget { final torStatus = ref.watch(torProxyServiceProvider).value; final activePort = ref.watch(searchProxyPortProvider); - final bootstrapProgress = torStatus?.bootstrapProgress ?? 0; - final showSpinner = - routeThroughTor && (activePort == null || bootstrapProgress < 100); + routeThroughTor && + (activePort == null || !(torStatus?.isReady ?? false)); // Push the latest native status into the stream on first build so the // bar reflects real progress even when the user lands on the search @@ -161,14 +161,13 @@ class WebSearchTorBootstrapProgress extends HookConsumerWidget { final torAsync = ref.watch(torProxyServiceProvider); final status = torAsync.value; - final isRunning = status?.isRunning ?? false; final bootstrapProgress = status?.bootstrapProgress ?? 0; // Hide once Tor is fully running and bootstrapped — otherwise, mirror // the Tor settings screen and always show a determinate bar (value // anchored to the live bootstrap progress, never indeterminate, so the // user can actually track the percentage). - if (isRunning && bootstrapProgress >= 100) return const SizedBox.shrink(); + if (status?.isReady ?? false) return const SizedBox.shrink(); final appColors = AppColors.of(context); return Padding( diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/search_filter_chips.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/search_filter_chips.dart index e8585a4d..904a42aa 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/search_filter_chips.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/search_filter_chips.dart @@ -19,7 +19,7 @@ */ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:weblibre/features/search_credits/domain/repositories/web_search_settings.dart'; import 'package:weblibre/features/web_search/data/locale_options.dart'; diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/search_mode_selector.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/search_mode_selector.dart index 4a3e7e6b..c072e74c 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/search_mode_selector.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/search_mode_selector.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:weblibre/features/search_credits/domain/repositories/web_search_settings.dart'; class SearchModeSelector extends ConsumerWidget { diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_card.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_card.dart index 22bc4863..ac71ec5d 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_card.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_card.dart @@ -6,7 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:nullability/nullability.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:weblibre/features/search_credits/domain/repositories/web_search_settings.dart'; import 'package:weblibre/features/web_search/domain/controllers/search_controller.dart'; import 'package:weblibre/features/web_search/domain/entities/captured_page_state.dart'; diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_metadata_chips.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_metadata_chips.dart index 4a46f15d..c524392c 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_metadata_chips.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_metadata_chips.dart @@ -4,7 +4,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:intl/locale.dart' as intl; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:weblibre/domain/repositories/locale_resolver.dart'; const _expandableKeys = {'snippet', 'review', 'question'}; diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/web_search_infobox_card.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/web_search_infobox_card.dart index 9d024933..a878131e 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/web_search_infobox_card.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/web_search_infobox_card.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:weblibre/features/web_search/domain/controllers/search_controller.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; diff --git a/apps/weblibre/lib/main.dart b/apps/weblibre/lib/main.dart index 285334bf..e5102ce7 100644 --- a/apps/weblibre/lib/main.dart +++ b/apps/weblibre/lib/main.dart @@ -44,12 +44,15 @@ import 'package:weblibre/core/providers/app_state.dart'; import 'package:weblibre/core/providers/defaults.dart'; import 'package:weblibre/core/providers/router.dart'; import 'package:weblibre/domain/services/app_initialization.dart'; -import 'package:weblibre/features/geckoview/features/browser/domain/services/engine_settings_replication.dart'; import 'package:weblibre/features/account/domain/services/account_callback_handler.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/services/engine_settings_replication.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart'; import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/services/local_index_pruner.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/services/local_index_settings_sync.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_logs.dart'; +import 'package:weblibre/features/proxy/domain/services/browser_dns_leak_guard.dart'; +import 'package:weblibre/features/proxy/domain/services/singbox_proxy_endpoint_sync.dart'; import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/web_feed/presentation/controllers/fetch_articles.dart'; @@ -123,6 +126,10 @@ class _MainWidget extends HookConsumerWidget { // Keep the sandbox capture controller alive for the lifetime of the app // so it can react to pigeon events even when no UI subscribes to it. ref.watch(sandboxCaptureControllerProvider); + // Keep proxy/Tor log subscriptions active from app start so startup + // messages reach the ring buffer before the browser view (or logs + // screen) mounts and would otherwise drop them. + ref.watch(singboxProxyLogsProvider.select((_) => null)); final rootKey = ref.watch(appStateKeyProvider); @@ -274,6 +281,17 @@ class _MainWidget extends HookConsumerWidget { // Activate account callback deep link handler ref.read(accountCallbackHandlerProvider); + + // Activate the proxy DNS leak guard: when any sing-box profile is + // running AND the user has opted in, GeckoView TRR is forced to + // off so browser DNS follows the SOCKS proxyDNS path instead of + // resolving outside the proxy. + ref.read(browserDnsLeakGuardProvider); + + // Mirror the sing-box runtime's SOCKS endpoints into Gecko's + // container-proxy registry. Side-effect-only notifier. + ref.read(singboxProxyEndpointSyncProvider); + if (!kDebugMode) { await BackgroundFetch.configure( BackgroundFetchConfig( diff --git a/apps/weblibre/lib/presentation/controllers/website_title.dart b/apps/weblibre/lib/presentation/controllers/website_title.dart index d03a046b..a8a6d201 100644 --- a/apps/weblibre/lib/presentation/controllers/website_title.dart +++ b/apps/weblibre/lib/presentation/controllers/website_title.dart @@ -28,9 +28,10 @@ import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; -import 'package:weblibre/features/user/data/models/tor_settings.dart'; -import 'package:weblibre/features/user/domain/repositories/tor_settings.dart'; +import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart'; part 'website_title.g.dart'; @@ -86,13 +87,19 @@ Future pageInfo( .read(tabDataRepositoryProvider.notifier) .getTabContainerData(tabState!.id); - final torSettings = ref.read(torSettingsWithDefaultsProvider); + final proxyRoutingSettings = ref.read( + proxyRoutingSettingsWithDefaultsProvider, + ); - if (containerData?.metadata.useProxy == true || + if (containerData?.metadata.proxyConnectionId is TorProxyConnectionId || (tabState.tabMode is! PrivateTabMode && - torSettings.proxyRegularTabsMode == TorRegularTabProxyMode.all) || + proxyRoutingSettings.regularTabsMode == + ProxyRegularTabRoutingMode.all && + proxyRoutingSettings.regularTabsProxyConnectionId + is TorProxyConnectionId) || (tabState.tabMode is PrivateTabMode && - torSettings.proxyPrivateTabsTor)) { + proxyRoutingSettings.privateTabsProxyConnectionId + is TorProxyConnectionId)) { proxyPort = await ref.read( torProxyServiceProvider.selectAsync((value) => value.socksPort), ); diff --git a/apps/weblibre/lib/presentation/controllers/website_title.g.dart b/apps/weblibre/lib/presentation/controllers/website_title.g.dart index 19758951..086cb0a3 100644 --- a/apps/weblibre/lib/presentation/controllers/website_title.g.dart +++ b/apps/weblibre/lib/presentation/controllers/website_title.g.dart @@ -162,7 +162,7 @@ final class PageInfoProvider } } -String _$pageInfoHash() => r'9a379657c8a3f353aba1dfb0f66b9100ce494fc5'; +String _$pageInfoHash() => r'1dedc23d2420c950c7cec57500e99c5c221ed6e2'; final class PageInfoFamily extends $Family with diff --git a/apps/weblibre/lib/presentation/widgets/button_spinner.dart b/apps/weblibre/lib/presentation/widgets/button_spinner.dart new file mode 100644 index 00000000..010d36bd --- /dev/null +++ b/apps/weblibre/lib/presentation/widgets/button_spinner.dart @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; + +class ButtonSpinner extends StatelessWidget { + final double dimension; + + const ButtonSpinner({super.key, this.dimension = 18}); + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: dimension, + child: const CircularProgressIndicator(strokeWidth: 2), + ); + } +} diff --git a/apps/weblibre/lib/presentation/widgets/obscurable_text_field.dart b/apps/weblibre/lib/presentation/widgets/obscurable_text_field.dart new file mode 100644 index 00000000..0bea0d4e --- /dev/null +++ b/apps/weblibre/lib/presentation/widgets/obscurable_text_field.dart @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +/// A [TextField] for secret values. Hidden by default; tap the trailing eye +/// icon to reveal. Obscuring forces single-line; [revealedMinLines] and +/// [revealedMaxLines] control how the field expands once revealed. +class ObscurableTextField extends HookWidget { + final TextEditingController? controller; + final bool enabled; + final InputDecoration decoration; + final TextInputAction? textInputAction; + final TextInputType? keyboardType; + final int? revealedMinLines; + final int? revealedMaxLines; + final ValueChanged? onChanged; + + const ObscurableTextField({ + super.key, + this.controller, + this.enabled = true, + this.decoration = const InputDecoration(), + this.textInputAction, + this.keyboardType, + this.revealedMinLines, + this.revealedMaxLines, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final obscured = useState(true); + + final suffix = IconButton( + tooltip: obscured.value ? 'Show' : 'Hide', + icon: Icon( + obscured.value + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: enabled ? () => obscured.value = !obscured.value : null, + ); + + return TextField( + controller: controller, + enabled: enabled, + obscureText: obscured.value, + keyboardType: keyboardType, + textInputAction: textInputAction, + minLines: obscured.value ? 1 : revealedMinLines, + maxLines: obscured.value ? 1 : (revealedMaxLines ?? 1), + autocorrect: false, + enableSuggestions: false, + decoration: decoration.copyWith(suffixIcon: suffix), + onChanged: onChanged, + ); + } +} diff --git a/apps/weblibre/lib/utils/ui_helper.dart b/apps/weblibre/lib/utils/ui_helper.dart index fba724bd..854319fe 100644 --- a/apps/weblibre/lib/utils/ui_helper.dart +++ b/apps/weblibre/lib/utils/ui_helper.dart @@ -43,6 +43,22 @@ SnackBar _createFloatingSnackBar({ ); } +/// Maximum number of lines an error/info snackbar will render before +/// truncating with an ellipsis. Long error messages (e.g. raw exception +/// dumps) would otherwise overflow the floating snackbar layout. +const int _kSnackBarMaxLines = 4; + +/// Maximum number of characters of a raw error string included in a +/// snackbar before it is truncated. Keeps stack-trace-like blobs from +/// dominating the screen while still leaving the full error available +/// in the log output. +const int _kSnackBarErrorMaxChars = 240; + +String _truncateForSnackBar(String message) { + if (message.length <= _kSnackBarErrorMaxChars) return message; + return '${message.substring(0, _kSnackBarErrorMaxChars).trimRight()}…'; +} + void showErrorMessage( BuildContext context, String message, { @@ -51,7 +67,9 @@ void showErrorMessage( }) { final snackBar = _createFloatingSnackBar( content: Text( - message, + _truncateForSnackBar(message), + maxLines: _kSnackBarMaxLines, + overflow: TextOverflow.ellipsis, style: TextStyle(color: Theme.of(context).colorScheme.error), ), backgroundColor: Theme.of(context).colorScheme.onError, @@ -70,7 +88,11 @@ void showInfoMessage( SnackBarAction? action, }) { final snackBar = _createFloatingSnackBar( - content: Text(message), + content: Text( + _truncateForSnackBar(message), + maxLines: _kSnackBarMaxLines, + overflow: TextOverflow.ellipsis, + ), action: action, duration: duration, persist: persist, diff --git a/apps/weblibre/pubspec.yaml b/apps/weblibre/pubspec.yaml index 20f3ee54..51e278d6 100644 --- a/apps/weblibre/pubspec.yaml +++ b/apps/weblibre/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: path: ../../packages/flutter_mozilla_components flutter_reorderable_grid_view: ^5.6.0 flutter_slidable: ^4.0.3 + flutter_singbox_proxy: + path: ../../packages/flutter_singbox_proxy flutter_svg: ^2.3.0 flutter_tor: path: ../../packages/flutter_tor @@ -69,8 +71,8 @@ dependencies: path: ../../../weblibre_account/packages/privacypass_client search_client: path: ../../../weblibre_account/packages/search_client - search_backend: - path: ../../../weblibre_account/apps/search_backend + search_protocol: + path: ../../../weblibre_account/packages/search_protocol pretty_qr_code: ^3.6.0 qr_code_scanner_plus: ^2.1.1 quick_actions: ^1.1.0 diff --git a/apps/weblibre/test/drift/user/generated/schema.dart b/apps/weblibre/test/drift/user/generated/schema.dart index 9dccfb29..f76d308b 100644 --- a/apps/weblibre/test/drift/user/generated/schema.dart +++ b/apps/weblibre/test/drift/user/generated/schema.dart @@ -9,6 +9,9 @@ import 'schema_v2.dart' as v2; import 'schema_v3.dart' as v3; import 'schema_v4.dart' as v4; import 'schema_v5.dart' as v5; +import 'schema_v6.dart' as v6; +import 'schema_v7.dart' as v7; +import 'schema_v8.dart' as v8; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -24,10 +27,16 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v4.DatabaseAtV4(db); case 5: return v5.DatabaseAtV5(db); + case 6: + return v6.DatabaseAtV6(db); + case 7: + return v7.DatabaseAtV7(db); + case 8: + return v8.DatabaseAtV8(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8]; } diff --git a/apps/weblibre/test/drift/user/generated/schema_v6.dart b/apps/weblibre/test/drift/user/generated/schema_v6.dart new file mode 100644 index 00000000..7c6c9c3c --- /dev/null +++ b/apps/weblibre/test/drift/user/generated/schema_v6.dart @@ -0,0 +1,2136 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Setting extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Setting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn partitionKey = GeneratedColumn( + 'partition_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + true, + type: DriftSqlType.any, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, partitionKey, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'setting'; + @override + Set get $primaryKey => {key}; + @override + SettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + partitionKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}partition_key'], + ), + value: attachedDatabase.typeMapping.read( + DriftSqlType.any, + data['${effectivePrefix}value'], + ), + ); + } + + @override + Setting createAlias(String alias) { + return Setting(attachedDatabase, alias); + } + + @override + bool get isStrict => true; + @override + bool get dontWriteConstraints => true; +} + +class SettingData extends DataClass implements Insertable { + final String key; + final String? partitionKey; + final DriftAny? value; + const SettingData({required this.key, this.partitionKey, this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + if (!nullToAbsent || partitionKey != null) { + map['partition_key'] = Variable(partitionKey); + } + if (!nullToAbsent || value != null) { + map['value'] = Variable(value); + } + return map; + } + + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingData( + key: serializer.fromJson(json['key']), + partitionKey: serializer.fromJson(json['partitionKey']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'partitionKey': serializer.toJson(partitionKey), + 'value': serializer.toJson(value), + }; + } + + SettingData copyWith({ + String? key, + Value partitionKey = const Value.absent(), + Value value = const Value.absent(), + }) => SettingData( + key: key ?? this.key, + partitionKey: partitionKey.present ? partitionKey.value : this.partitionKey, + value: value.present ? value.value : this.value, + ); + SettingData copyWithCompanion(SettingCompanion data) { + return SettingData( + key: data.key.present ? data.key.value : this.key, + partitionKey: data.partitionKey.present + ? data.partitionKey.value + : this.partitionKey, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingData(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, partitionKey, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingData && + other.key == this.key && + other.partitionKey == this.partitionKey && + other.value == this.value); +} + +class SettingCompanion extends UpdateCompanion { + final Value key; + final Value partitionKey; + final Value value; + final Value rowid; + const SettingCompanion({ + this.key = const Value.absent(), + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingCompanion.insert({ + required String key, + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }) : key = Value(key); + static Insertable custom({ + Expression? key, + Expression? partitionKey, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (partitionKey != null) 'partition_key': partitionKey, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingCompanion copyWith({ + Value? key, + Value? partitionKey, + Value? value, + Value? rowid, + }) { + return SettingCompanion( + key: key ?? this.key, + partitionKey: partitionKey ?? this.partitionKey, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (partitionKey.present) { + map['partition_key'] = Variable(partitionKey.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingCompanion(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ProxyProfile extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ProxyProfile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn configJson = GeneratedColumn( + 'config_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + @override + List get $columns => [ + id, + name, + type, + configJson, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'proxy_profile'; + @override + Set get $primaryKey => {id}; + @override + ProxyProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProxyProfileData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + configJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}config_json'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + ProxyProfile createAlias(String alias) { + return ProxyProfile(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ProxyProfileData extends DataClass + implements Insertable { + final String id; + final String name; + final String type; + final String configJson; + final int createdAt; + final int updatedAt; + const ProxyProfileData({ + required this.id, + required this.name, + required this.type, + required this.configJson, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['type'] = Variable(type); + map['config_json'] = Variable(configJson); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + factory ProxyProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProxyProfileData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + configJson: serializer.fromJson(json['configJson']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'configJson': serializer.toJson(configJson), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + ProxyProfileData copyWith({ + String? id, + String? name, + String? type, + String? configJson, + int? createdAt, + int? updatedAt, + }) => ProxyProfileData( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + configJson: configJson ?? this.configJson, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + ProxyProfileData copyWithCompanion(ProxyProfileCompanion data) { + return ProxyProfileData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + configJson: data.configJson.present + ? data.configJson.value + : this.configJson, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ProxyProfileData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('configJson: $configJson, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, type, configJson, createdAt, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProxyProfileData && + other.id == this.id && + other.name == this.name && + other.type == this.type && + other.configJson == this.configJson && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class ProxyProfileCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value type; + final Value configJson; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const ProxyProfileCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.type = const Value.absent(), + this.configJson = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProxyProfileCompanion.insert({ + required String id, + required String name, + required String type, + required String configJson, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + type = Value(type), + configJson = Value(configJson); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? type, + Expression? configJson, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (type != null) 'type': type, + if (configJson != null) 'config_json': configJson, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ProxyProfileCompanion copyWith({ + Value? id, + Value? name, + Value? type, + Value? configJson, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return ProxyProfileCompanion( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + configJson: configJson ?? this.configJson, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (configJson.present) { + map['config_json'] = Variable(configJson.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProxyProfileCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('configJson: $configJson, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ProxyRoutingSetting extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ProxyRoutingSetting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY CHECK (id = 1)', + ); + late final GeneratedColumn regularTabsMode = GeneratedColumn( + 'regular_tabs_mode', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn regularTabsProxyConnectionId = + GeneratedColumn( + 'regular_tabs_proxy_connection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn privateTabsProxyConnectionId = + GeneratedColumn( + 'private_tabs_proxy_connection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [ + id, + regularTabsMode, + regularTabsProxyConnectionId, + privateTabsProxyConnectionId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'proxy_routing_setting'; + @override + Set get $primaryKey => {id}; + @override + ProxyRoutingSettingData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProxyRoutingSettingData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + regularTabsMode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}regular_tabs_mode'], + )!, + regularTabsProxyConnectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}regular_tabs_proxy_connection_id'], + ), + privateTabsProxyConnectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}private_tabs_proxy_connection_id'], + ), + ); + } + + @override + ProxyRoutingSetting createAlias(String alias) { + return ProxyRoutingSetting(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ProxyRoutingSettingData extends DataClass + implements Insertable { + final int id; + final String regularTabsMode; + final String? regularTabsProxyConnectionId; + final String? privateTabsProxyConnectionId; + const ProxyRoutingSettingData({ + required this.id, + required this.regularTabsMode, + this.regularTabsProxyConnectionId, + this.privateTabsProxyConnectionId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['regular_tabs_mode'] = Variable(regularTabsMode); + if (!nullToAbsent || regularTabsProxyConnectionId != null) { + map['regular_tabs_proxy_connection_id'] = Variable( + regularTabsProxyConnectionId, + ); + } + if (!nullToAbsent || privateTabsProxyConnectionId != null) { + map['private_tabs_proxy_connection_id'] = Variable( + privateTabsProxyConnectionId, + ); + } + return map; + } + + factory ProxyRoutingSettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProxyRoutingSettingData( + id: serializer.fromJson(json['id']), + regularTabsMode: serializer.fromJson(json['regularTabsMode']), + regularTabsProxyConnectionId: serializer.fromJson( + json['regularTabsProxyConnectionId'], + ), + privateTabsProxyConnectionId: serializer.fromJson( + json['privateTabsProxyConnectionId'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'regularTabsMode': serializer.toJson(regularTabsMode), + 'regularTabsProxyConnectionId': serializer.toJson( + regularTabsProxyConnectionId, + ), + 'privateTabsProxyConnectionId': serializer.toJson( + privateTabsProxyConnectionId, + ), + }; + } + + ProxyRoutingSettingData copyWith({ + int? id, + String? regularTabsMode, + Value regularTabsProxyConnectionId = const Value.absent(), + Value privateTabsProxyConnectionId = const Value.absent(), + }) => ProxyRoutingSettingData( + id: id ?? this.id, + regularTabsMode: regularTabsMode ?? this.regularTabsMode, + regularTabsProxyConnectionId: regularTabsProxyConnectionId.present + ? regularTabsProxyConnectionId.value + : this.regularTabsProxyConnectionId, + privateTabsProxyConnectionId: privateTabsProxyConnectionId.present + ? privateTabsProxyConnectionId.value + : this.privateTabsProxyConnectionId, + ); + ProxyRoutingSettingData copyWithCompanion(ProxyRoutingSettingCompanion data) { + return ProxyRoutingSettingData( + id: data.id.present ? data.id.value : this.id, + regularTabsMode: data.regularTabsMode.present + ? data.regularTabsMode.value + : this.regularTabsMode, + regularTabsProxyConnectionId: data.regularTabsProxyConnectionId.present + ? data.regularTabsProxyConnectionId.value + : this.regularTabsProxyConnectionId, + privateTabsProxyConnectionId: data.privateTabsProxyConnectionId.present + ? data.privateTabsProxyConnectionId.value + : this.privateTabsProxyConnectionId, + ); + } + + @override + String toString() { + return (StringBuffer('ProxyRoutingSettingData(') + ..write('id: $id, ') + ..write('regularTabsMode: $regularTabsMode, ') + ..write( + 'regularTabsProxyConnectionId: $regularTabsProxyConnectionId, ', + ) + ..write('privateTabsProxyConnectionId: $privateTabsProxyConnectionId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + regularTabsMode, + regularTabsProxyConnectionId, + privateTabsProxyConnectionId, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProxyRoutingSettingData && + other.id == this.id && + other.regularTabsMode == this.regularTabsMode && + other.regularTabsProxyConnectionId == + this.regularTabsProxyConnectionId && + other.privateTabsProxyConnectionId == + this.privateTabsProxyConnectionId); +} + +class ProxyRoutingSettingCompanion + extends UpdateCompanion { + final Value id; + final Value regularTabsMode; + final Value regularTabsProxyConnectionId; + final Value privateTabsProxyConnectionId; + const ProxyRoutingSettingCompanion({ + this.id = const Value.absent(), + this.regularTabsMode = const Value.absent(), + this.regularTabsProxyConnectionId = const Value.absent(), + this.privateTabsProxyConnectionId = const Value.absent(), + }); + ProxyRoutingSettingCompanion.insert({ + this.id = const Value.absent(), + required String regularTabsMode, + this.regularTabsProxyConnectionId = const Value.absent(), + this.privateTabsProxyConnectionId = const Value.absent(), + }) : regularTabsMode = Value(regularTabsMode); + static Insertable custom({ + Expression? id, + Expression? regularTabsMode, + Expression? regularTabsProxyConnectionId, + Expression? privateTabsProxyConnectionId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (regularTabsMode != null) 'regular_tabs_mode': regularTabsMode, + if (regularTabsProxyConnectionId != null) + 'regular_tabs_proxy_connection_id': regularTabsProxyConnectionId, + if (privateTabsProxyConnectionId != null) + 'private_tabs_proxy_connection_id': privateTabsProxyConnectionId, + }); + } + + ProxyRoutingSettingCompanion copyWith({ + Value? id, + Value? regularTabsMode, + Value? regularTabsProxyConnectionId, + Value? privateTabsProxyConnectionId, + }) { + return ProxyRoutingSettingCompanion( + id: id ?? this.id, + regularTabsMode: regularTabsMode ?? this.regularTabsMode, + regularTabsProxyConnectionId: + regularTabsProxyConnectionId ?? this.regularTabsProxyConnectionId, + privateTabsProxyConnectionId: + privateTabsProxyConnectionId ?? this.privateTabsProxyConnectionId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (regularTabsMode.present) { + map['regular_tabs_mode'] = Variable(regularTabsMode.value); + } + if (regularTabsProxyConnectionId.present) { + map['regular_tabs_proxy_connection_id'] = Variable( + regularTabsProxyConnectionId.value, + ); + } + if (privateTabsProxyConnectionId.present) { + map['private_tabs_proxy_connection_id'] = Variable( + privateTabsProxyConnectionId.value, + ); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProxyRoutingSettingCompanion(') + ..write('id: $id, ') + ..write('regularTabsMode: $regularTabsMode, ') + ..write( + 'regularTabsProxyConnectionId: $regularTabsProxyConnectionId, ', + ) + ..write('privateTabsProxyConnectionId: $privateTabsProxyConnectionId') + ..write(')')) + .toString(); + } +} + +class IconCache extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + IconCache(this.attachedDatabase, [this._alias]); + late final GeneratedColumn origin = GeneratedColumn( + 'origin', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn iconData = + GeneratedColumn( + 'icon_data', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn fetchDate = GeneratedColumn( + 'fetch_date', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [origin, iconData, fetchDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'icon_cache'; + @override + Set get $primaryKey => {origin}; + @override + IconCacheData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return IconCacheData( + origin: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}origin'], + )!, + iconData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}icon_data'], + )!, + fetchDate: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}fetch_date'], + )!, + ); + } + + @override + IconCache createAlias(String alias) { + return IconCache(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class IconCacheData extends DataClass implements Insertable { + final String origin; + final i2.Uint8List iconData; + final int fetchDate; + const IconCacheData({ + required this.origin, + required this.iconData, + required this.fetchDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['origin'] = Variable(origin); + map['icon_data'] = Variable(iconData); + map['fetch_date'] = Variable(fetchDate); + return map; + } + + factory IconCacheData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return IconCacheData( + origin: serializer.fromJson(json['origin']), + iconData: serializer.fromJson(json['iconData']), + fetchDate: serializer.fromJson(json['fetchDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'origin': serializer.toJson(origin), + 'iconData': serializer.toJson(iconData), + 'fetchDate': serializer.toJson(fetchDate), + }; + } + + IconCacheData copyWith({ + String? origin, + i2.Uint8List? iconData, + int? fetchDate, + }) => IconCacheData( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + ); + IconCacheData copyWithCompanion(IconCacheCompanion data) { + return IconCacheData( + origin: data.origin.present ? data.origin.value : this.origin, + iconData: data.iconData.present ? data.iconData.value : this.iconData, + fetchDate: data.fetchDate.present ? data.fetchDate.value : this.fetchDate, + ); + } + + @override + String toString() { + return (StringBuffer('IconCacheData(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(origin, $driftBlobEquality.hash(iconData), fetchDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is IconCacheData && + other.origin == this.origin && + $driftBlobEquality.equals(other.iconData, this.iconData) && + other.fetchDate == this.fetchDate); +} + +class IconCacheCompanion extends UpdateCompanion { + final Value origin; + final Value iconData; + final Value fetchDate; + final Value rowid; + const IconCacheCompanion({ + this.origin = const Value.absent(), + this.iconData = const Value.absent(), + this.fetchDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + IconCacheCompanion.insert({ + required String origin, + required i2.Uint8List iconData, + required int fetchDate, + this.rowid = const Value.absent(), + }) : origin = Value(origin), + iconData = Value(iconData), + fetchDate = Value(fetchDate); + static Insertable custom({ + Expression? origin, + Expression? iconData, + Expression? fetchDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (origin != null) 'origin': origin, + if (iconData != null) 'icon_data': iconData, + if (fetchDate != null) 'fetch_date': fetchDate, + if (rowid != null) 'rowid': rowid, + }); + } + + IconCacheCompanion copyWith({ + Value? origin, + Value? iconData, + Value? fetchDate, + Value? rowid, + }) { + return IconCacheCompanion( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (origin.present) { + map['origin'] = Variable(origin.value); + } + if (iconData.present) { + map['icon_data'] = Variable(iconData.value); + } + if (fetchDate.present) { + map['fetch_date'] = Variable(fetchDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('IconCacheCompanion(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Onboarding extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Onboarding(this.attachedDatabase, [this._alias]); + late final GeneratedColumn revision = GeneratedColumn( + 'revision', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn completionDate = GeneratedColumn( + 'completion_date', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [revision, completionDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'onboarding'; + @override + Set get $primaryKey => const {}; + @override + OnboardingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OnboardingData( + revision: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}revision'], + )!, + completionDate: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}completion_date'], + )!, + ); + } + + @override + Onboarding createAlias(String alias) { + return Onboarding(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class OnboardingData extends DataClass implements Insertable { + final int revision; + final int completionDate; + const OnboardingData({required this.revision, required this.completionDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['revision'] = Variable(revision); + map['completion_date'] = Variable(completionDate); + return map; + } + + factory OnboardingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OnboardingData( + revision: serializer.fromJson(json['revision']), + completionDate: serializer.fromJson(json['completionDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'revision': serializer.toJson(revision), + 'completionDate': serializer.toJson(completionDate), + }; + } + + OnboardingData copyWith({int? revision, int? completionDate}) => + OnboardingData( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + ); + OnboardingData copyWithCompanion(OnboardingCompanion data) { + return OnboardingData( + revision: data.revision.present ? data.revision.value : this.revision, + completionDate: data.completionDate.present + ? data.completionDate.value + : this.completionDate, + ); + } + + @override + String toString() { + return (StringBuffer('OnboardingData(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(revision, completionDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OnboardingData && + other.revision == this.revision && + other.completionDate == this.completionDate); +} + +class OnboardingCompanion extends UpdateCompanion { + final Value revision; + final Value completionDate; + final Value rowid; + const OnboardingCompanion({ + this.revision = const Value.absent(), + this.completionDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + OnboardingCompanion.insert({ + required int revision, + required int completionDate, + this.rowid = const Value.absent(), + }) : revision = Value(revision), + completionDate = Value(completionDate); + static Insertable custom({ + Expression? revision, + Expression? completionDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (revision != null) 'revision': revision, + if (completionDate != null) 'completion_date': completionDate, + if (rowid != null) 'rowid': rowid, + }); + } + + OnboardingCompanion copyWith({ + Value? revision, + Value? completionDate, + Value? rowid, + }) { + return OnboardingCompanion( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (revision.present) { + map['revision'] = Variable(revision.value); + } + if (completionDate.present) { + map['completion_date'] = Variable(completionDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OnboardingCompanion(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Riverpod extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Riverpod(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn json = GeneratedColumn( + 'json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn expireAt = GeneratedColumn( + 'expireAt', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn destroyKey = GeneratedColumn( + 'destroyKey', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, json, expireAt, destroyKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'riverpod'; + @override + Set get $primaryKey => {key}; + @override + RiverpodData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RiverpodData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + json: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}json'], + )!, + expireAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}expireAt'], + ), + destroyKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}destroyKey'], + ), + ); + } + + @override + Riverpod createAlias(String alias) { + return Riverpod(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get dontWriteConstraints => true; +} + +class RiverpodData extends DataClass implements Insertable { + final String key; + final String json; + final int? expireAt; + final String? destroyKey; + const RiverpodData({ + required this.key, + required this.json, + this.expireAt, + this.destroyKey, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['json'] = Variable(json); + if (!nullToAbsent || expireAt != null) { + map['expireAt'] = Variable(expireAt); + } + if (!nullToAbsent || destroyKey != null) { + map['destroyKey'] = Variable(destroyKey); + } + return map; + } + + factory RiverpodData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RiverpodData( + key: serializer.fromJson(json['key']), + json: serializer.fromJson(json['json']), + expireAt: serializer.fromJson(json['expireAt']), + destroyKey: serializer.fromJson(json['destroyKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'json': serializer.toJson(json), + 'expireAt': serializer.toJson(expireAt), + 'destroyKey': serializer.toJson(destroyKey), + }; + } + + RiverpodData copyWith({ + String? key, + String? json, + Value expireAt = const Value.absent(), + Value destroyKey = const Value.absent(), + }) => RiverpodData( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt.present ? expireAt.value : this.expireAt, + destroyKey: destroyKey.present ? destroyKey.value : this.destroyKey, + ); + RiverpodData copyWithCompanion(RiverpodCompanion data) { + return RiverpodData( + key: data.key.present ? data.key.value : this.key, + json: data.json.present ? data.json.value : this.json, + expireAt: data.expireAt.present ? data.expireAt.value : this.expireAt, + destroyKey: data.destroyKey.present + ? data.destroyKey.value + : this.destroyKey, + ); + } + + @override + String toString() { + return (StringBuffer('RiverpodData(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, json, expireAt, destroyKey); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RiverpodData && + other.key == this.key && + other.json == this.json && + other.expireAt == this.expireAt && + other.destroyKey == this.destroyKey); +} + +class RiverpodCompanion extends UpdateCompanion { + final Value key; + final Value json; + final Value expireAt; + final Value destroyKey; + const RiverpodCompanion({ + this.key = const Value.absent(), + this.json = const Value.absent(), + this.expireAt = const Value.absent(), + this.destroyKey = const Value.absent(), + }); + RiverpodCompanion.insert({ + required String key, + required String json, + this.expireAt = const Value.absent(), + this.destroyKey = const Value.absent(), + }) : key = Value(key), + json = Value(json); + static Insertable custom({ + Expression? key, + Expression? json, + Expression? expireAt, + Expression? destroyKey, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (json != null) 'json': json, + if (expireAt != null) 'expireAt': expireAt, + if (destroyKey != null) 'destroyKey': destroyKey, + }); + } + + RiverpodCompanion copyWith({ + Value? key, + Value? json, + Value? expireAt, + Value? destroyKey, + }) { + return RiverpodCompanion( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt ?? this.expireAt, + destroyKey: destroyKey ?? this.destroyKey, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (json.present) { + map['json'] = Variable(json.value); + } + if (expireAt.present) { + map['expireAt'] = Variable(expireAt.value); + } + if (destroyKey.present) { + map['destroyKey'] = Variable(destroyKey.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RiverpodCompanion(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } +} + +class ToolbarButtonConfigs extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ToolbarButtonConfigs(this.attachedDatabase, [this._alias]); + late final GeneratedColumn buttonId = GeneratedColumn( + 'button_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY', + ); + late final GeneratedColumn orderKey = GeneratedColumn( + 'order_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isVisible = GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT TRUE', + defaultValue: const CustomExpression('TRUE'), + ); + late final GeneratedColumn fallbackId = GeneratedColumn( + 'fallback_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL', + ); + @override + List get $columns => [ + buttonId, + orderKey, + isVisible, + fallbackId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'toolbar_button_configs'; + @override + Set get $primaryKey => {buttonId}; + @override + ToolbarButtonConfigsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ToolbarButtonConfigsData( + buttonId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}button_id'], + )!, + orderKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_key'], + )!, + isVisible: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_visible'], + )!, + fallbackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}fallback_id'], + ), + ); + } + + @override + ToolbarButtonConfigs createAlias(String alias) { + return ToolbarButtonConfigs(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ToolbarButtonConfigsData extends DataClass + implements Insertable { + final String buttonId; + final String orderKey; + final int isVisible; + final String? fallbackId; + const ToolbarButtonConfigsData({ + required this.buttonId, + required this.orderKey, + required this.isVisible, + this.fallbackId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['button_id'] = Variable(buttonId); + map['order_key'] = Variable(orderKey); + map['is_visible'] = Variable(isVisible); + if (!nullToAbsent || fallbackId != null) { + map['fallback_id'] = Variable(fallbackId); + } + return map; + } + + factory ToolbarButtonConfigsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ToolbarButtonConfigsData( + buttonId: serializer.fromJson(json['buttonId']), + orderKey: serializer.fromJson(json['orderKey']), + isVisible: serializer.fromJson(json['isVisible']), + fallbackId: serializer.fromJson(json['fallbackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'buttonId': serializer.toJson(buttonId), + 'orderKey': serializer.toJson(orderKey), + 'isVisible': serializer.toJson(isVisible), + 'fallbackId': serializer.toJson(fallbackId), + }; + } + + ToolbarButtonConfigsData copyWith({ + String? buttonId, + String? orderKey, + int? isVisible, + Value fallbackId = const Value.absent(), + }) => ToolbarButtonConfigsData( + buttonId: buttonId ?? this.buttonId, + orderKey: orderKey ?? this.orderKey, + isVisible: isVisible ?? this.isVisible, + fallbackId: fallbackId.present ? fallbackId.value : this.fallbackId, + ); + ToolbarButtonConfigsData copyWithCompanion( + ToolbarButtonConfigsCompanion data, + ) { + return ToolbarButtonConfigsData( + buttonId: data.buttonId.present ? data.buttonId.value : this.buttonId, + orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + fallbackId: data.fallbackId.present + ? data.fallbackId.value + : this.fallbackId, + ); + } + + @override + String toString() { + return (StringBuffer('ToolbarButtonConfigsData(') + ..write('buttonId: $buttonId, ') + ..write('orderKey: $orderKey, ') + ..write('isVisible: $isVisible, ') + ..write('fallbackId: $fallbackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(buttonId, orderKey, isVisible, fallbackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ToolbarButtonConfigsData && + other.buttonId == this.buttonId && + other.orderKey == this.orderKey && + other.isVisible == this.isVisible && + other.fallbackId == this.fallbackId); +} + +class ToolbarButtonConfigsCompanion + extends UpdateCompanion { + final Value buttonId; + final Value orderKey; + final Value isVisible; + final Value fallbackId; + final Value rowid; + const ToolbarButtonConfigsCompanion({ + this.buttonId = const Value.absent(), + this.orderKey = const Value.absent(), + this.isVisible = const Value.absent(), + this.fallbackId = const Value.absent(), + this.rowid = const Value.absent(), + }); + ToolbarButtonConfigsCompanion.insert({ + required String buttonId, + required String orderKey, + this.isVisible = const Value.absent(), + this.fallbackId = const Value.absent(), + this.rowid = const Value.absent(), + }) : buttonId = Value(buttonId), + orderKey = Value(orderKey); + static Insertable custom({ + Expression? buttonId, + Expression? orderKey, + Expression? isVisible, + Expression? fallbackId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (buttonId != null) 'button_id': buttonId, + if (orderKey != null) 'order_key': orderKey, + if (isVisible != null) 'is_visible': isVisible, + if (fallbackId != null) 'fallback_id': fallbackId, + if (rowid != null) 'rowid': rowid, + }); + } + + ToolbarButtonConfigsCompanion copyWith({ + Value? buttonId, + Value? orderKey, + Value? isVisible, + Value? fallbackId, + Value? rowid, + }) { + return ToolbarButtonConfigsCompanion( + buttonId: buttonId ?? this.buttonId, + orderKey: orderKey ?? this.orderKey, + isVisible: isVisible ?? this.isVisible, + fallbackId: fallbackId ?? this.fallbackId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (buttonId.present) { + map['button_id'] = Variable(buttonId.value); + } + if (orderKey.present) { + map['order_key'] = Variable(orderKey.value); + } + if (isVisible.present) { + map['is_visible'] = Variable(isVisible.value); + } + if (fallbackId.present) { + map['fallback_id'] = Variable(fallbackId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ToolbarButtonConfigsCompanion(') + ..write('buttonId: $buttonId, ') + ..write('orderKey: $orderKey, ') + ..write('isVisible: $isVisible, ') + ..write('fallbackId: $fallbackId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SearchTokens extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SearchTokens(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn token = + GeneratedColumn( + 'token', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn insertedAt = GeneratedColumn( + 'inserted_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn issuerKeyVersion = GeneratedColumn( + 'issuer_key_version', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn reservedAt = GeneratedColumn( + 'reserved_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [ + id, + token, + insertedAt, + issuerKeyVersion, + reservedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'search_tokens'; + @override + Set get $primaryKey => {id}; + @override + SearchTokensData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SearchTokensData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + token: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}token'], + )!, + insertedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}inserted_at'], + )!, + issuerKeyVersion: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}issuer_key_version'], + )!, + reservedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}reserved_at'], + ), + ); + } + + @override + SearchTokens createAlias(String alias) { + return SearchTokens(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class SearchTokensData extends DataClass + implements Insertable { + final int id; + final i2.Uint8List token; + final int insertedAt; + final String issuerKeyVersion; + final int? reservedAt; + const SearchTokensData({ + required this.id, + required this.token, + required this.insertedAt, + required this.issuerKeyVersion, + this.reservedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['token'] = Variable(token); + map['inserted_at'] = Variable(insertedAt); + map['issuer_key_version'] = Variable(issuerKeyVersion); + if (!nullToAbsent || reservedAt != null) { + map['reserved_at'] = Variable(reservedAt); + } + return map; + } + + factory SearchTokensData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SearchTokensData( + id: serializer.fromJson(json['id']), + token: serializer.fromJson(json['token']), + insertedAt: serializer.fromJson(json['insertedAt']), + issuerKeyVersion: serializer.fromJson(json['issuerKeyVersion']), + reservedAt: serializer.fromJson(json['reservedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'token': serializer.toJson(token), + 'insertedAt': serializer.toJson(insertedAt), + 'issuerKeyVersion': serializer.toJson(issuerKeyVersion), + 'reservedAt': serializer.toJson(reservedAt), + }; + } + + SearchTokensData copyWith({ + int? id, + i2.Uint8List? token, + int? insertedAt, + String? issuerKeyVersion, + Value reservedAt = const Value.absent(), + }) => SearchTokensData( + id: id ?? this.id, + token: token ?? this.token, + insertedAt: insertedAt ?? this.insertedAt, + issuerKeyVersion: issuerKeyVersion ?? this.issuerKeyVersion, + reservedAt: reservedAt.present ? reservedAt.value : this.reservedAt, + ); + SearchTokensData copyWithCompanion(SearchTokensCompanion data) { + return SearchTokensData( + id: data.id.present ? data.id.value : this.id, + token: data.token.present ? data.token.value : this.token, + insertedAt: data.insertedAt.present + ? data.insertedAt.value + : this.insertedAt, + issuerKeyVersion: data.issuerKeyVersion.present + ? data.issuerKeyVersion.value + : this.issuerKeyVersion, + reservedAt: data.reservedAt.present + ? data.reservedAt.value + : this.reservedAt, + ); + } + + @override + String toString() { + return (StringBuffer('SearchTokensData(') + ..write('id: $id, ') + ..write('token: $token, ') + ..write('insertedAt: $insertedAt, ') + ..write('issuerKeyVersion: $issuerKeyVersion, ') + ..write('reservedAt: $reservedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + $driftBlobEquality.hash(token), + insertedAt, + issuerKeyVersion, + reservedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SearchTokensData && + other.id == this.id && + $driftBlobEquality.equals(other.token, this.token) && + other.insertedAt == this.insertedAt && + other.issuerKeyVersion == this.issuerKeyVersion && + other.reservedAt == this.reservedAt); +} + +class SearchTokensCompanion extends UpdateCompanion { + final Value id; + final Value token; + final Value insertedAt; + final Value issuerKeyVersion; + final Value reservedAt; + const SearchTokensCompanion({ + this.id = const Value.absent(), + this.token = const Value.absent(), + this.insertedAt = const Value.absent(), + this.issuerKeyVersion = const Value.absent(), + this.reservedAt = const Value.absent(), + }); + SearchTokensCompanion.insert({ + this.id = const Value.absent(), + required i2.Uint8List token, + required int insertedAt, + required String issuerKeyVersion, + this.reservedAt = const Value.absent(), + }) : token = Value(token), + insertedAt = Value(insertedAt), + issuerKeyVersion = Value(issuerKeyVersion); + static Insertable custom({ + Expression? id, + Expression? token, + Expression? insertedAt, + Expression? issuerKeyVersion, + Expression? reservedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (token != null) 'token': token, + if (insertedAt != null) 'inserted_at': insertedAt, + if (issuerKeyVersion != null) 'issuer_key_version': issuerKeyVersion, + if (reservedAt != null) 'reserved_at': reservedAt, + }); + } + + SearchTokensCompanion copyWith({ + Value? id, + Value? token, + Value? insertedAt, + Value? issuerKeyVersion, + Value? reservedAt, + }) { + return SearchTokensCompanion( + id: id ?? this.id, + token: token ?? this.token, + insertedAt: insertedAt ?? this.insertedAt, + issuerKeyVersion: issuerKeyVersion ?? this.issuerKeyVersion, + reservedAt: reservedAt ?? this.reservedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (token.present) { + map['token'] = Variable(token.value); + } + if (insertedAt.present) { + map['inserted_at'] = Variable(insertedAt.value); + } + if (issuerKeyVersion.present) { + map['issuer_key_version'] = Variable(issuerKeyVersion.value); + } + if (reservedAt.present) { + map['reserved_at'] = Variable(reservedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SearchTokensCompanion(') + ..write('id: $id, ') + ..write('token: $token, ') + ..write('insertedAt: $insertedAt, ') + ..write('issuerKeyVersion: $issuerKeyVersion, ') + ..write('reservedAt: $reservedAt') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV6 extends GeneratedDatabase { + DatabaseAtV6(QueryExecutor e) : super(e); + late final Setting setting = Setting(this); + late final ProxyProfile proxyProfile = ProxyProfile(this); + late final Index idxProxyProfileUpdatedAt = Index( + 'idx_proxy_profile_updated_at', + 'CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)', + ); + late final ProxyRoutingSetting proxyRoutingSetting = ProxyRoutingSetting( + this, + ); + late final IconCache iconCache = IconCache(this); + late final Onboarding onboarding = Onboarding(this); + late final Riverpod riverpod = Riverpod(this); + late final ToolbarButtonConfigs toolbarButtonConfigs = ToolbarButtonConfigs( + this, + ); + late final Index idxToolbarOrderKey = Index( + 'idx_toolbar_order_key', + 'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)', + ); + late final SearchTokens searchTokens = SearchTokens(this); + late final Index idxSearchTokensInsertedAt = Index( + 'idx_search_tokens_inserted_at', + 'CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)', + ); + late final Index idxSearchTokensReservedAt = Index( + 'idx_search_tokens_reserved_at', + 'CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + setting, + proxyProfile, + idxProxyProfileUpdatedAt, + proxyRoutingSetting, + iconCache, + onboarding, + riverpod, + toolbarButtonConfigs, + idxToolbarOrderKey, + searchTokens, + idxSearchTokensInsertedAt, + idxSearchTokensReservedAt, + ]; + @override + int get schemaVersion => 6; +} diff --git a/apps/weblibre/test/drift/user/generated/schema_v7.dart b/apps/weblibre/test/drift/user/generated/schema_v7.dart new file mode 100644 index 00000000..f352dfc0 --- /dev/null +++ b/apps/weblibre/test/drift/user/generated/schema_v7.dart @@ -0,0 +1,2183 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Setting extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Setting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn partitionKey = GeneratedColumn( + 'partition_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + true, + type: DriftSqlType.any, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, partitionKey, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'setting'; + @override + Set get $primaryKey => {key}; + @override + SettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + partitionKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}partition_key'], + ), + value: attachedDatabase.typeMapping.read( + DriftSqlType.any, + data['${effectivePrefix}value'], + ), + ); + } + + @override + Setting createAlias(String alias) { + return Setting(attachedDatabase, alias); + } + + @override + bool get isStrict => true; + @override + bool get dontWriteConstraints => true; +} + +class SettingData extends DataClass implements Insertable { + final String key; + final String? partitionKey; + final DriftAny? value; + const SettingData({required this.key, this.partitionKey, this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + if (!nullToAbsent || partitionKey != null) { + map['partition_key'] = Variable(partitionKey); + } + if (!nullToAbsent || value != null) { + map['value'] = Variable(value); + } + return map; + } + + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingData( + key: serializer.fromJson(json['key']), + partitionKey: serializer.fromJson(json['partitionKey']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'partitionKey': serializer.toJson(partitionKey), + 'value': serializer.toJson(value), + }; + } + + SettingData copyWith({ + String? key, + Value partitionKey = const Value.absent(), + Value value = const Value.absent(), + }) => SettingData( + key: key ?? this.key, + partitionKey: partitionKey.present ? partitionKey.value : this.partitionKey, + value: value.present ? value.value : this.value, + ); + SettingData copyWithCompanion(SettingCompanion data) { + return SettingData( + key: data.key.present ? data.key.value : this.key, + partitionKey: data.partitionKey.present + ? data.partitionKey.value + : this.partitionKey, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingData(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, partitionKey, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingData && + other.key == this.key && + other.partitionKey == this.partitionKey && + other.value == this.value); +} + +class SettingCompanion extends UpdateCompanion { + final Value key; + final Value partitionKey; + final Value value; + final Value rowid; + const SettingCompanion({ + this.key = const Value.absent(), + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingCompanion.insert({ + required String key, + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }) : key = Value(key); + static Insertable custom({ + Expression? key, + Expression? partitionKey, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (partitionKey != null) 'partition_key': partitionKey, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingCompanion copyWith({ + Value? key, + Value? partitionKey, + Value? value, + Value? rowid, + }) { + return SettingCompanion( + key: key ?? this.key, + partitionKey: partitionKey ?? this.partitionKey, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (partitionKey.present) { + map['partition_key'] = Variable(partitionKey.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingCompanion(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ProxyProfile extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ProxyProfile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn configJson = GeneratedColumn( + 'config_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn dnsOverrideJson = GeneratedColumn( + 'dns_override_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + @override + List get $columns => [ + id, + name, + type, + configJson, + dnsOverrideJson, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'proxy_profile'; + @override + Set get $primaryKey => {id}; + @override + ProxyProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProxyProfileData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + configJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}config_json'], + )!, + dnsOverrideJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dns_override_json'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + ProxyProfile createAlias(String alias) { + return ProxyProfile(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ProxyProfileData extends DataClass + implements Insertable { + final String id; + final String name; + final String type; + final String configJson; + final String? dnsOverrideJson; + final int createdAt; + final int updatedAt; + const ProxyProfileData({ + required this.id, + required this.name, + required this.type, + required this.configJson, + this.dnsOverrideJson, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['type'] = Variable(type); + map['config_json'] = Variable(configJson); + if (!nullToAbsent || dnsOverrideJson != null) { + map['dns_override_json'] = Variable(dnsOverrideJson); + } + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + factory ProxyProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProxyProfileData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + configJson: serializer.fromJson(json['configJson']), + dnsOverrideJson: serializer.fromJson(json['dnsOverrideJson']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'configJson': serializer.toJson(configJson), + 'dnsOverrideJson': serializer.toJson(dnsOverrideJson), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + ProxyProfileData copyWith({ + String? id, + String? name, + String? type, + String? configJson, + Value dnsOverrideJson = const Value.absent(), + int? createdAt, + int? updatedAt, + }) => ProxyProfileData( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + configJson: configJson ?? this.configJson, + dnsOverrideJson: dnsOverrideJson.present + ? dnsOverrideJson.value + : this.dnsOverrideJson, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + ProxyProfileData copyWithCompanion(ProxyProfileCompanion data) { + return ProxyProfileData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + configJson: data.configJson.present + ? data.configJson.value + : this.configJson, + dnsOverrideJson: data.dnsOverrideJson.present + ? data.dnsOverrideJson.value + : this.dnsOverrideJson, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ProxyProfileData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('configJson: $configJson, ') + ..write('dnsOverrideJson: $dnsOverrideJson, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + type, + configJson, + dnsOverrideJson, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProxyProfileData && + other.id == this.id && + other.name == this.name && + other.type == this.type && + other.configJson == this.configJson && + other.dnsOverrideJson == this.dnsOverrideJson && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class ProxyProfileCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value type; + final Value configJson; + final Value dnsOverrideJson; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const ProxyProfileCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.type = const Value.absent(), + this.configJson = const Value.absent(), + this.dnsOverrideJson = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProxyProfileCompanion.insert({ + required String id, + required String name, + required String type, + required String configJson, + this.dnsOverrideJson = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + type = Value(type), + configJson = Value(configJson); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? type, + Expression? configJson, + Expression? dnsOverrideJson, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (type != null) 'type': type, + if (configJson != null) 'config_json': configJson, + if (dnsOverrideJson != null) 'dns_override_json': dnsOverrideJson, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ProxyProfileCompanion copyWith({ + Value? id, + Value? name, + Value? type, + Value? configJson, + Value? dnsOverrideJson, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return ProxyProfileCompanion( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + configJson: configJson ?? this.configJson, + dnsOverrideJson: dnsOverrideJson ?? this.dnsOverrideJson, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (configJson.present) { + map['config_json'] = Variable(configJson.value); + } + if (dnsOverrideJson.present) { + map['dns_override_json'] = Variable(dnsOverrideJson.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProxyProfileCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('configJson: $configJson, ') + ..write('dnsOverrideJson: $dnsOverrideJson, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ProxyRoutingSetting extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ProxyRoutingSetting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY CHECK (id = 1)', + ); + late final GeneratedColumn regularTabsMode = GeneratedColumn( + 'regular_tabs_mode', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn regularTabsProxyConnectionId = + GeneratedColumn( + 'regular_tabs_proxy_connection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn privateTabsProxyConnectionId = + GeneratedColumn( + 'private_tabs_proxy_connection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [ + id, + regularTabsMode, + regularTabsProxyConnectionId, + privateTabsProxyConnectionId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'proxy_routing_setting'; + @override + Set get $primaryKey => {id}; + @override + ProxyRoutingSettingData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProxyRoutingSettingData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + regularTabsMode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}regular_tabs_mode'], + )!, + regularTabsProxyConnectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}regular_tabs_proxy_connection_id'], + ), + privateTabsProxyConnectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}private_tabs_proxy_connection_id'], + ), + ); + } + + @override + ProxyRoutingSetting createAlias(String alias) { + return ProxyRoutingSetting(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ProxyRoutingSettingData extends DataClass + implements Insertable { + final int id; + final String regularTabsMode; + final String? regularTabsProxyConnectionId; + final String? privateTabsProxyConnectionId; + const ProxyRoutingSettingData({ + required this.id, + required this.regularTabsMode, + this.regularTabsProxyConnectionId, + this.privateTabsProxyConnectionId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['regular_tabs_mode'] = Variable(regularTabsMode); + if (!nullToAbsent || regularTabsProxyConnectionId != null) { + map['regular_tabs_proxy_connection_id'] = Variable( + regularTabsProxyConnectionId, + ); + } + if (!nullToAbsent || privateTabsProxyConnectionId != null) { + map['private_tabs_proxy_connection_id'] = Variable( + privateTabsProxyConnectionId, + ); + } + return map; + } + + factory ProxyRoutingSettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProxyRoutingSettingData( + id: serializer.fromJson(json['id']), + regularTabsMode: serializer.fromJson(json['regularTabsMode']), + regularTabsProxyConnectionId: serializer.fromJson( + json['regularTabsProxyConnectionId'], + ), + privateTabsProxyConnectionId: serializer.fromJson( + json['privateTabsProxyConnectionId'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'regularTabsMode': serializer.toJson(regularTabsMode), + 'regularTabsProxyConnectionId': serializer.toJson( + regularTabsProxyConnectionId, + ), + 'privateTabsProxyConnectionId': serializer.toJson( + privateTabsProxyConnectionId, + ), + }; + } + + ProxyRoutingSettingData copyWith({ + int? id, + String? regularTabsMode, + Value regularTabsProxyConnectionId = const Value.absent(), + Value privateTabsProxyConnectionId = const Value.absent(), + }) => ProxyRoutingSettingData( + id: id ?? this.id, + regularTabsMode: regularTabsMode ?? this.regularTabsMode, + regularTabsProxyConnectionId: regularTabsProxyConnectionId.present + ? regularTabsProxyConnectionId.value + : this.regularTabsProxyConnectionId, + privateTabsProxyConnectionId: privateTabsProxyConnectionId.present + ? privateTabsProxyConnectionId.value + : this.privateTabsProxyConnectionId, + ); + ProxyRoutingSettingData copyWithCompanion(ProxyRoutingSettingCompanion data) { + return ProxyRoutingSettingData( + id: data.id.present ? data.id.value : this.id, + regularTabsMode: data.regularTabsMode.present + ? data.regularTabsMode.value + : this.regularTabsMode, + regularTabsProxyConnectionId: data.regularTabsProxyConnectionId.present + ? data.regularTabsProxyConnectionId.value + : this.regularTabsProxyConnectionId, + privateTabsProxyConnectionId: data.privateTabsProxyConnectionId.present + ? data.privateTabsProxyConnectionId.value + : this.privateTabsProxyConnectionId, + ); + } + + @override + String toString() { + return (StringBuffer('ProxyRoutingSettingData(') + ..write('id: $id, ') + ..write('regularTabsMode: $regularTabsMode, ') + ..write( + 'regularTabsProxyConnectionId: $regularTabsProxyConnectionId, ', + ) + ..write('privateTabsProxyConnectionId: $privateTabsProxyConnectionId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + regularTabsMode, + regularTabsProxyConnectionId, + privateTabsProxyConnectionId, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProxyRoutingSettingData && + other.id == this.id && + other.regularTabsMode == this.regularTabsMode && + other.regularTabsProxyConnectionId == + this.regularTabsProxyConnectionId && + other.privateTabsProxyConnectionId == + this.privateTabsProxyConnectionId); +} + +class ProxyRoutingSettingCompanion + extends UpdateCompanion { + final Value id; + final Value regularTabsMode; + final Value regularTabsProxyConnectionId; + final Value privateTabsProxyConnectionId; + const ProxyRoutingSettingCompanion({ + this.id = const Value.absent(), + this.regularTabsMode = const Value.absent(), + this.regularTabsProxyConnectionId = const Value.absent(), + this.privateTabsProxyConnectionId = const Value.absent(), + }); + ProxyRoutingSettingCompanion.insert({ + this.id = const Value.absent(), + required String regularTabsMode, + this.regularTabsProxyConnectionId = const Value.absent(), + this.privateTabsProxyConnectionId = const Value.absent(), + }) : regularTabsMode = Value(regularTabsMode); + static Insertable custom({ + Expression? id, + Expression? regularTabsMode, + Expression? regularTabsProxyConnectionId, + Expression? privateTabsProxyConnectionId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (regularTabsMode != null) 'regular_tabs_mode': regularTabsMode, + if (regularTabsProxyConnectionId != null) + 'regular_tabs_proxy_connection_id': regularTabsProxyConnectionId, + if (privateTabsProxyConnectionId != null) + 'private_tabs_proxy_connection_id': privateTabsProxyConnectionId, + }); + } + + ProxyRoutingSettingCompanion copyWith({ + Value? id, + Value? regularTabsMode, + Value? regularTabsProxyConnectionId, + Value? privateTabsProxyConnectionId, + }) { + return ProxyRoutingSettingCompanion( + id: id ?? this.id, + regularTabsMode: regularTabsMode ?? this.regularTabsMode, + regularTabsProxyConnectionId: + regularTabsProxyConnectionId ?? this.regularTabsProxyConnectionId, + privateTabsProxyConnectionId: + privateTabsProxyConnectionId ?? this.privateTabsProxyConnectionId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (regularTabsMode.present) { + map['regular_tabs_mode'] = Variable(regularTabsMode.value); + } + if (regularTabsProxyConnectionId.present) { + map['regular_tabs_proxy_connection_id'] = Variable( + regularTabsProxyConnectionId.value, + ); + } + if (privateTabsProxyConnectionId.present) { + map['private_tabs_proxy_connection_id'] = Variable( + privateTabsProxyConnectionId.value, + ); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProxyRoutingSettingCompanion(') + ..write('id: $id, ') + ..write('regularTabsMode: $regularTabsMode, ') + ..write( + 'regularTabsProxyConnectionId: $regularTabsProxyConnectionId, ', + ) + ..write('privateTabsProxyConnectionId: $privateTabsProxyConnectionId') + ..write(')')) + .toString(); + } +} + +class IconCache extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + IconCache(this.attachedDatabase, [this._alias]); + late final GeneratedColumn origin = GeneratedColumn( + 'origin', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn iconData = + GeneratedColumn( + 'icon_data', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn fetchDate = GeneratedColumn( + 'fetch_date', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [origin, iconData, fetchDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'icon_cache'; + @override + Set get $primaryKey => {origin}; + @override + IconCacheData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return IconCacheData( + origin: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}origin'], + )!, + iconData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}icon_data'], + )!, + fetchDate: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}fetch_date'], + )!, + ); + } + + @override + IconCache createAlias(String alias) { + return IconCache(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class IconCacheData extends DataClass implements Insertable { + final String origin; + final i2.Uint8List iconData; + final int fetchDate; + const IconCacheData({ + required this.origin, + required this.iconData, + required this.fetchDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['origin'] = Variable(origin); + map['icon_data'] = Variable(iconData); + map['fetch_date'] = Variable(fetchDate); + return map; + } + + factory IconCacheData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return IconCacheData( + origin: serializer.fromJson(json['origin']), + iconData: serializer.fromJson(json['iconData']), + fetchDate: serializer.fromJson(json['fetchDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'origin': serializer.toJson(origin), + 'iconData': serializer.toJson(iconData), + 'fetchDate': serializer.toJson(fetchDate), + }; + } + + IconCacheData copyWith({ + String? origin, + i2.Uint8List? iconData, + int? fetchDate, + }) => IconCacheData( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + ); + IconCacheData copyWithCompanion(IconCacheCompanion data) { + return IconCacheData( + origin: data.origin.present ? data.origin.value : this.origin, + iconData: data.iconData.present ? data.iconData.value : this.iconData, + fetchDate: data.fetchDate.present ? data.fetchDate.value : this.fetchDate, + ); + } + + @override + String toString() { + return (StringBuffer('IconCacheData(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(origin, $driftBlobEquality.hash(iconData), fetchDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is IconCacheData && + other.origin == this.origin && + $driftBlobEquality.equals(other.iconData, this.iconData) && + other.fetchDate == this.fetchDate); +} + +class IconCacheCompanion extends UpdateCompanion { + final Value origin; + final Value iconData; + final Value fetchDate; + final Value rowid; + const IconCacheCompanion({ + this.origin = const Value.absent(), + this.iconData = const Value.absent(), + this.fetchDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + IconCacheCompanion.insert({ + required String origin, + required i2.Uint8List iconData, + required int fetchDate, + this.rowid = const Value.absent(), + }) : origin = Value(origin), + iconData = Value(iconData), + fetchDate = Value(fetchDate); + static Insertable custom({ + Expression? origin, + Expression? iconData, + Expression? fetchDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (origin != null) 'origin': origin, + if (iconData != null) 'icon_data': iconData, + if (fetchDate != null) 'fetch_date': fetchDate, + if (rowid != null) 'rowid': rowid, + }); + } + + IconCacheCompanion copyWith({ + Value? origin, + Value? iconData, + Value? fetchDate, + Value? rowid, + }) { + return IconCacheCompanion( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (origin.present) { + map['origin'] = Variable(origin.value); + } + if (iconData.present) { + map['icon_data'] = Variable(iconData.value); + } + if (fetchDate.present) { + map['fetch_date'] = Variable(fetchDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('IconCacheCompanion(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Onboarding extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Onboarding(this.attachedDatabase, [this._alias]); + late final GeneratedColumn revision = GeneratedColumn( + 'revision', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn completionDate = GeneratedColumn( + 'completion_date', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [revision, completionDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'onboarding'; + @override + Set get $primaryKey => const {}; + @override + OnboardingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OnboardingData( + revision: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}revision'], + )!, + completionDate: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}completion_date'], + )!, + ); + } + + @override + Onboarding createAlias(String alias) { + return Onboarding(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class OnboardingData extends DataClass implements Insertable { + final int revision; + final int completionDate; + const OnboardingData({required this.revision, required this.completionDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['revision'] = Variable(revision); + map['completion_date'] = Variable(completionDate); + return map; + } + + factory OnboardingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OnboardingData( + revision: serializer.fromJson(json['revision']), + completionDate: serializer.fromJson(json['completionDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'revision': serializer.toJson(revision), + 'completionDate': serializer.toJson(completionDate), + }; + } + + OnboardingData copyWith({int? revision, int? completionDate}) => + OnboardingData( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + ); + OnboardingData copyWithCompanion(OnboardingCompanion data) { + return OnboardingData( + revision: data.revision.present ? data.revision.value : this.revision, + completionDate: data.completionDate.present + ? data.completionDate.value + : this.completionDate, + ); + } + + @override + String toString() { + return (StringBuffer('OnboardingData(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(revision, completionDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OnboardingData && + other.revision == this.revision && + other.completionDate == this.completionDate); +} + +class OnboardingCompanion extends UpdateCompanion { + final Value revision; + final Value completionDate; + final Value rowid; + const OnboardingCompanion({ + this.revision = const Value.absent(), + this.completionDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + OnboardingCompanion.insert({ + required int revision, + required int completionDate, + this.rowid = const Value.absent(), + }) : revision = Value(revision), + completionDate = Value(completionDate); + static Insertable custom({ + Expression? revision, + Expression? completionDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (revision != null) 'revision': revision, + if (completionDate != null) 'completion_date': completionDate, + if (rowid != null) 'rowid': rowid, + }); + } + + OnboardingCompanion copyWith({ + Value? revision, + Value? completionDate, + Value? rowid, + }) { + return OnboardingCompanion( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (revision.present) { + map['revision'] = Variable(revision.value); + } + if (completionDate.present) { + map['completion_date'] = Variable(completionDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OnboardingCompanion(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Riverpod extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Riverpod(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn json = GeneratedColumn( + 'json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn expireAt = GeneratedColumn( + 'expireAt', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn destroyKey = GeneratedColumn( + 'destroyKey', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, json, expireAt, destroyKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'riverpod'; + @override + Set get $primaryKey => {key}; + @override + RiverpodData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RiverpodData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + json: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}json'], + )!, + expireAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}expireAt'], + ), + destroyKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}destroyKey'], + ), + ); + } + + @override + Riverpod createAlias(String alias) { + return Riverpod(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get dontWriteConstraints => true; +} + +class RiverpodData extends DataClass implements Insertable { + final String key; + final String json; + final int? expireAt; + final String? destroyKey; + const RiverpodData({ + required this.key, + required this.json, + this.expireAt, + this.destroyKey, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['json'] = Variable(json); + if (!nullToAbsent || expireAt != null) { + map['expireAt'] = Variable(expireAt); + } + if (!nullToAbsent || destroyKey != null) { + map['destroyKey'] = Variable(destroyKey); + } + return map; + } + + factory RiverpodData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RiverpodData( + key: serializer.fromJson(json['key']), + json: serializer.fromJson(json['json']), + expireAt: serializer.fromJson(json['expireAt']), + destroyKey: serializer.fromJson(json['destroyKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'json': serializer.toJson(json), + 'expireAt': serializer.toJson(expireAt), + 'destroyKey': serializer.toJson(destroyKey), + }; + } + + RiverpodData copyWith({ + String? key, + String? json, + Value expireAt = const Value.absent(), + Value destroyKey = const Value.absent(), + }) => RiverpodData( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt.present ? expireAt.value : this.expireAt, + destroyKey: destroyKey.present ? destroyKey.value : this.destroyKey, + ); + RiverpodData copyWithCompanion(RiverpodCompanion data) { + return RiverpodData( + key: data.key.present ? data.key.value : this.key, + json: data.json.present ? data.json.value : this.json, + expireAt: data.expireAt.present ? data.expireAt.value : this.expireAt, + destroyKey: data.destroyKey.present + ? data.destroyKey.value + : this.destroyKey, + ); + } + + @override + String toString() { + return (StringBuffer('RiverpodData(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, json, expireAt, destroyKey); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RiverpodData && + other.key == this.key && + other.json == this.json && + other.expireAt == this.expireAt && + other.destroyKey == this.destroyKey); +} + +class RiverpodCompanion extends UpdateCompanion { + final Value key; + final Value json; + final Value expireAt; + final Value destroyKey; + const RiverpodCompanion({ + this.key = const Value.absent(), + this.json = const Value.absent(), + this.expireAt = const Value.absent(), + this.destroyKey = const Value.absent(), + }); + RiverpodCompanion.insert({ + required String key, + required String json, + this.expireAt = const Value.absent(), + this.destroyKey = const Value.absent(), + }) : key = Value(key), + json = Value(json); + static Insertable custom({ + Expression? key, + Expression? json, + Expression? expireAt, + Expression? destroyKey, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (json != null) 'json': json, + if (expireAt != null) 'expireAt': expireAt, + if (destroyKey != null) 'destroyKey': destroyKey, + }); + } + + RiverpodCompanion copyWith({ + Value? key, + Value? json, + Value? expireAt, + Value? destroyKey, + }) { + return RiverpodCompanion( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt ?? this.expireAt, + destroyKey: destroyKey ?? this.destroyKey, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (json.present) { + map['json'] = Variable(json.value); + } + if (expireAt.present) { + map['expireAt'] = Variable(expireAt.value); + } + if (destroyKey.present) { + map['destroyKey'] = Variable(destroyKey.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RiverpodCompanion(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } +} + +class ToolbarButtonConfigs extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ToolbarButtonConfigs(this.attachedDatabase, [this._alias]); + late final GeneratedColumn buttonId = GeneratedColumn( + 'button_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY', + ); + late final GeneratedColumn orderKey = GeneratedColumn( + 'order_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isVisible = GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT TRUE', + defaultValue: const CustomExpression('TRUE'), + ); + late final GeneratedColumn fallbackId = GeneratedColumn( + 'fallback_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL', + ); + @override + List get $columns => [ + buttonId, + orderKey, + isVisible, + fallbackId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'toolbar_button_configs'; + @override + Set get $primaryKey => {buttonId}; + @override + ToolbarButtonConfigsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ToolbarButtonConfigsData( + buttonId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}button_id'], + )!, + orderKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_key'], + )!, + isVisible: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_visible'], + )!, + fallbackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}fallback_id'], + ), + ); + } + + @override + ToolbarButtonConfigs createAlias(String alias) { + return ToolbarButtonConfigs(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ToolbarButtonConfigsData extends DataClass + implements Insertable { + final String buttonId; + final String orderKey; + final int isVisible; + final String? fallbackId; + const ToolbarButtonConfigsData({ + required this.buttonId, + required this.orderKey, + required this.isVisible, + this.fallbackId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['button_id'] = Variable(buttonId); + map['order_key'] = Variable(orderKey); + map['is_visible'] = Variable(isVisible); + if (!nullToAbsent || fallbackId != null) { + map['fallback_id'] = Variable(fallbackId); + } + return map; + } + + factory ToolbarButtonConfigsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ToolbarButtonConfigsData( + buttonId: serializer.fromJson(json['buttonId']), + orderKey: serializer.fromJson(json['orderKey']), + isVisible: serializer.fromJson(json['isVisible']), + fallbackId: serializer.fromJson(json['fallbackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'buttonId': serializer.toJson(buttonId), + 'orderKey': serializer.toJson(orderKey), + 'isVisible': serializer.toJson(isVisible), + 'fallbackId': serializer.toJson(fallbackId), + }; + } + + ToolbarButtonConfigsData copyWith({ + String? buttonId, + String? orderKey, + int? isVisible, + Value fallbackId = const Value.absent(), + }) => ToolbarButtonConfigsData( + buttonId: buttonId ?? this.buttonId, + orderKey: orderKey ?? this.orderKey, + isVisible: isVisible ?? this.isVisible, + fallbackId: fallbackId.present ? fallbackId.value : this.fallbackId, + ); + ToolbarButtonConfigsData copyWithCompanion( + ToolbarButtonConfigsCompanion data, + ) { + return ToolbarButtonConfigsData( + buttonId: data.buttonId.present ? data.buttonId.value : this.buttonId, + orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + fallbackId: data.fallbackId.present + ? data.fallbackId.value + : this.fallbackId, + ); + } + + @override + String toString() { + return (StringBuffer('ToolbarButtonConfigsData(') + ..write('buttonId: $buttonId, ') + ..write('orderKey: $orderKey, ') + ..write('isVisible: $isVisible, ') + ..write('fallbackId: $fallbackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(buttonId, orderKey, isVisible, fallbackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ToolbarButtonConfigsData && + other.buttonId == this.buttonId && + other.orderKey == this.orderKey && + other.isVisible == this.isVisible && + other.fallbackId == this.fallbackId); +} + +class ToolbarButtonConfigsCompanion + extends UpdateCompanion { + final Value buttonId; + final Value orderKey; + final Value isVisible; + final Value fallbackId; + final Value rowid; + const ToolbarButtonConfigsCompanion({ + this.buttonId = const Value.absent(), + this.orderKey = const Value.absent(), + this.isVisible = const Value.absent(), + this.fallbackId = const Value.absent(), + this.rowid = const Value.absent(), + }); + ToolbarButtonConfigsCompanion.insert({ + required String buttonId, + required String orderKey, + this.isVisible = const Value.absent(), + this.fallbackId = const Value.absent(), + this.rowid = const Value.absent(), + }) : buttonId = Value(buttonId), + orderKey = Value(orderKey); + static Insertable custom({ + Expression? buttonId, + Expression? orderKey, + Expression? isVisible, + Expression? fallbackId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (buttonId != null) 'button_id': buttonId, + if (orderKey != null) 'order_key': orderKey, + if (isVisible != null) 'is_visible': isVisible, + if (fallbackId != null) 'fallback_id': fallbackId, + if (rowid != null) 'rowid': rowid, + }); + } + + ToolbarButtonConfigsCompanion copyWith({ + Value? buttonId, + Value? orderKey, + Value? isVisible, + Value? fallbackId, + Value? rowid, + }) { + return ToolbarButtonConfigsCompanion( + buttonId: buttonId ?? this.buttonId, + orderKey: orderKey ?? this.orderKey, + isVisible: isVisible ?? this.isVisible, + fallbackId: fallbackId ?? this.fallbackId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (buttonId.present) { + map['button_id'] = Variable(buttonId.value); + } + if (orderKey.present) { + map['order_key'] = Variable(orderKey.value); + } + if (isVisible.present) { + map['is_visible'] = Variable(isVisible.value); + } + if (fallbackId.present) { + map['fallback_id'] = Variable(fallbackId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ToolbarButtonConfigsCompanion(') + ..write('buttonId: $buttonId, ') + ..write('orderKey: $orderKey, ') + ..write('isVisible: $isVisible, ') + ..write('fallbackId: $fallbackId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SearchTokens extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SearchTokens(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn token = + GeneratedColumn( + 'token', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn insertedAt = GeneratedColumn( + 'inserted_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn issuerKeyVersion = GeneratedColumn( + 'issuer_key_version', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn reservedAt = GeneratedColumn( + 'reserved_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [ + id, + token, + insertedAt, + issuerKeyVersion, + reservedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'search_tokens'; + @override + Set get $primaryKey => {id}; + @override + SearchTokensData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SearchTokensData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + token: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}token'], + )!, + insertedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}inserted_at'], + )!, + issuerKeyVersion: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}issuer_key_version'], + )!, + reservedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}reserved_at'], + ), + ); + } + + @override + SearchTokens createAlias(String alias) { + return SearchTokens(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class SearchTokensData extends DataClass + implements Insertable { + final int id; + final i2.Uint8List token; + final int insertedAt; + final String issuerKeyVersion; + final int? reservedAt; + const SearchTokensData({ + required this.id, + required this.token, + required this.insertedAt, + required this.issuerKeyVersion, + this.reservedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['token'] = Variable(token); + map['inserted_at'] = Variable(insertedAt); + map['issuer_key_version'] = Variable(issuerKeyVersion); + if (!nullToAbsent || reservedAt != null) { + map['reserved_at'] = Variable(reservedAt); + } + return map; + } + + factory SearchTokensData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SearchTokensData( + id: serializer.fromJson(json['id']), + token: serializer.fromJson(json['token']), + insertedAt: serializer.fromJson(json['insertedAt']), + issuerKeyVersion: serializer.fromJson(json['issuerKeyVersion']), + reservedAt: serializer.fromJson(json['reservedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'token': serializer.toJson(token), + 'insertedAt': serializer.toJson(insertedAt), + 'issuerKeyVersion': serializer.toJson(issuerKeyVersion), + 'reservedAt': serializer.toJson(reservedAt), + }; + } + + SearchTokensData copyWith({ + int? id, + i2.Uint8List? token, + int? insertedAt, + String? issuerKeyVersion, + Value reservedAt = const Value.absent(), + }) => SearchTokensData( + id: id ?? this.id, + token: token ?? this.token, + insertedAt: insertedAt ?? this.insertedAt, + issuerKeyVersion: issuerKeyVersion ?? this.issuerKeyVersion, + reservedAt: reservedAt.present ? reservedAt.value : this.reservedAt, + ); + SearchTokensData copyWithCompanion(SearchTokensCompanion data) { + return SearchTokensData( + id: data.id.present ? data.id.value : this.id, + token: data.token.present ? data.token.value : this.token, + insertedAt: data.insertedAt.present + ? data.insertedAt.value + : this.insertedAt, + issuerKeyVersion: data.issuerKeyVersion.present + ? data.issuerKeyVersion.value + : this.issuerKeyVersion, + reservedAt: data.reservedAt.present + ? data.reservedAt.value + : this.reservedAt, + ); + } + + @override + String toString() { + return (StringBuffer('SearchTokensData(') + ..write('id: $id, ') + ..write('token: $token, ') + ..write('insertedAt: $insertedAt, ') + ..write('issuerKeyVersion: $issuerKeyVersion, ') + ..write('reservedAt: $reservedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + $driftBlobEquality.hash(token), + insertedAt, + issuerKeyVersion, + reservedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SearchTokensData && + other.id == this.id && + $driftBlobEquality.equals(other.token, this.token) && + other.insertedAt == this.insertedAt && + other.issuerKeyVersion == this.issuerKeyVersion && + other.reservedAt == this.reservedAt); +} + +class SearchTokensCompanion extends UpdateCompanion { + final Value id; + final Value token; + final Value insertedAt; + final Value issuerKeyVersion; + final Value reservedAt; + const SearchTokensCompanion({ + this.id = const Value.absent(), + this.token = const Value.absent(), + this.insertedAt = const Value.absent(), + this.issuerKeyVersion = const Value.absent(), + this.reservedAt = const Value.absent(), + }); + SearchTokensCompanion.insert({ + this.id = const Value.absent(), + required i2.Uint8List token, + required int insertedAt, + required String issuerKeyVersion, + this.reservedAt = const Value.absent(), + }) : token = Value(token), + insertedAt = Value(insertedAt), + issuerKeyVersion = Value(issuerKeyVersion); + static Insertable custom({ + Expression? id, + Expression? token, + Expression? insertedAt, + Expression? issuerKeyVersion, + Expression? reservedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (token != null) 'token': token, + if (insertedAt != null) 'inserted_at': insertedAt, + if (issuerKeyVersion != null) 'issuer_key_version': issuerKeyVersion, + if (reservedAt != null) 'reserved_at': reservedAt, + }); + } + + SearchTokensCompanion copyWith({ + Value? id, + Value? token, + Value? insertedAt, + Value? issuerKeyVersion, + Value? reservedAt, + }) { + return SearchTokensCompanion( + id: id ?? this.id, + token: token ?? this.token, + insertedAt: insertedAt ?? this.insertedAt, + issuerKeyVersion: issuerKeyVersion ?? this.issuerKeyVersion, + reservedAt: reservedAt ?? this.reservedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (token.present) { + map['token'] = Variable(token.value); + } + if (insertedAt.present) { + map['inserted_at'] = Variable(insertedAt.value); + } + if (issuerKeyVersion.present) { + map['issuer_key_version'] = Variable(issuerKeyVersion.value); + } + if (reservedAt.present) { + map['reserved_at'] = Variable(reservedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SearchTokensCompanion(') + ..write('id: $id, ') + ..write('token: $token, ') + ..write('insertedAt: $insertedAt, ') + ..write('issuerKeyVersion: $issuerKeyVersion, ') + ..write('reservedAt: $reservedAt') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV7 extends GeneratedDatabase { + DatabaseAtV7(QueryExecutor e) : super(e); + late final Setting setting = Setting(this); + late final ProxyProfile proxyProfile = ProxyProfile(this); + late final Index idxProxyProfileUpdatedAt = Index( + 'idx_proxy_profile_updated_at', + 'CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)', + ); + late final ProxyRoutingSetting proxyRoutingSetting = ProxyRoutingSetting( + this, + ); + late final IconCache iconCache = IconCache(this); + late final Onboarding onboarding = Onboarding(this); + late final Riverpod riverpod = Riverpod(this); + late final ToolbarButtonConfigs toolbarButtonConfigs = ToolbarButtonConfigs( + this, + ); + late final Index idxToolbarOrderKey = Index( + 'idx_toolbar_order_key', + 'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)', + ); + late final SearchTokens searchTokens = SearchTokens(this); + late final Index idxSearchTokensInsertedAt = Index( + 'idx_search_tokens_inserted_at', + 'CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)', + ); + late final Index idxSearchTokensReservedAt = Index( + 'idx_search_tokens_reserved_at', + 'CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + setting, + proxyProfile, + idxProxyProfileUpdatedAt, + proxyRoutingSetting, + iconCache, + onboarding, + riverpod, + toolbarButtonConfigs, + idxToolbarOrderKey, + searchTokens, + idxSearchTokensInsertedAt, + idxSearchTokensReservedAt, + ]; + @override + int get schemaVersion => 7; +} diff --git a/apps/weblibre/test/drift/user/generated/schema_v8.dart b/apps/weblibre/test/drift/user/generated/schema_v8.dart new file mode 100644 index 00000000..f7d6db5c --- /dev/null +++ b/apps/weblibre/test/drift/user/generated/schema_v8.dart @@ -0,0 +1,1881 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Setting extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Setting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn partitionKey = GeneratedColumn( + 'partition_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + true, + type: DriftSqlType.any, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, partitionKey, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'setting'; + @override + Set get $primaryKey => {key}; + @override + SettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + partitionKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}partition_key'], + ), + value: attachedDatabase.typeMapping.read( + DriftSqlType.any, + data['${effectivePrefix}value'], + ), + ); + } + + @override + Setting createAlias(String alias) { + return Setting(attachedDatabase, alias); + } + + @override + bool get isStrict => true; + @override + bool get dontWriteConstraints => true; +} + +class SettingData extends DataClass implements Insertable { + final String key; + final String? partitionKey; + final DriftAny? value; + const SettingData({required this.key, this.partitionKey, this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + if (!nullToAbsent || partitionKey != null) { + map['partition_key'] = Variable(partitionKey); + } + if (!nullToAbsent || value != null) { + map['value'] = Variable(value); + } + return map; + } + + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingData( + key: serializer.fromJson(json['key']), + partitionKey: serializer.fromJson(json['partitionKey']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'partitionKey': serializer.toJson(partitionKey), + 'value': serializer.toJson(value), + }; + } + + SettingData copyWith({ + String? key, + Value partitionKey = const Value.absent(), + Value value = const Value.absent(), + }) => SettingData( + key: key ?? this.key, + partitionKey: partitionKey.present ? partitionKey.value : this.partitionKey, + value: value.present ? value.value : this.value, + ); + SettingData copyWithCompanion(SettingCompanion data) { + return SettingData( + key: data.key.present ? data.key.value : this.key, + partitionKey: data.partitionKey.present + ? data.partitionKey.value + : this.partitionKey, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingData(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, partitionKey, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingData && + other.key == this.key && + other.partitionKey == this.partitionKey && + other.value == this.value); +} + +class SettingCompanion extends UpdateCompanion { + final Value key; + final Value partitionKey; + final Value value; + final Value rowid; + const SettingCompanion({ + this.key = const Value.absent(), + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingCompanion.insert({ + required String key, + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }) : key = Value(key); + static Insertable custom({ + Expression? key, + Expression? partitionKey, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (partitionKey != null) 'partition_key': partitionKey, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingCompanion copyWith({ + Value? key, + Value? partitionKey, + Value? value, + Value? rowid, + }) { + return SettingCompanion( + key: key ?? this.key, + partitionKey: partitionKey ?? this.partitionKey, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (partitionKey.present) { + map['partition_key'] = Variable(partitionKey.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingCompanion(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ProxyProfile extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ProxyProfile(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn configJson = GeneratedColumn( + 'config_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn dnsOverrideJson = GeneratedColumn( + 'dns_override_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + @override + List get $columns => [ + id, + name, + type, + configJson, + dnsOverrideJson, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'proxy_profile'; + @override + Set get $primaryKey => {id}; + @override + ProxyProfileData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProxyProfileData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + configJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}config_json'], + )!, + dnsOverrideJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dns_override_json'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + ProxyProfile createAlias(String alias) { + return ProxyProfile(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ProxyProfileData extends DataClass + implements Insertable { + final String id; + final String name; + final String type; + final String configJson; + final String? dnsOverrideJson; + final int createdAt; + final int updatedAt; + const ProxyProfileData({ + required this.id, + required this.name, + required this.type, + required this.configJson, + this.dnsOverrideJson, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['type'] = Variable(type); + map['config_json'] = Variable(configJson); + if (!nullToAbsent || dnsOverrideJson != null) { + map['dns_override_json'] = Variable(dnsOverrideJson); + } + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + factory ProxyProfileData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProxyProfileData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + configJson: serializer.fromJson(json['configJson']), + dnsOverrideJson: serializer.fromJson(json['dnsOverrideJson']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'configJson': serializer.toJson(configJson), + 'dnsOverrideJson': serializer.toJson(dnsOverrideJson), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + ProxyProfileData copyWith({ + String? id, + String? name, + String? type, + String? configJson, + Value dnsOverrideJson = const Value.absent(), + int? createdAt, + int? updatedAt, + }) => ProxyProfileData( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + configJson: configJson ?? this.configJson, + dnsOverrideJson: dnsOverrideJson.present + ? dnsOverrideJson.value + : this.dnsOverrideJson, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + ProxyProfileData copyWithCompanion(ProxyProfileCompanion data) { + return ProxyProfileData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + configJson: data.configJson.present + ? data.configJson.value + : this.configJson, + dnsOverrideJson: data.dnsOverrideJson.present + ? data.dnsOverrideJson.value + : this.dnsOverrideJson, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ProxyProfileData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('configJson: $configJson, ') + ..write('dnsOverrideJson: $dnsOverrideJson, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + type, + configJson, + dnsOverrideJson, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProxyProfileData && + other.id == this.id && + other.name == this.name && + other.type == this.type && + other.configJson == this.configJson && + other.dnsOverrideJson == this.dnsOverrideJson && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class ProxyProfileCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value type; + final Value configJson; + final Value dnsOverrideJson; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const ProxyProfileCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.type = const Value.absent(), + this.configJson = const Value.absent(), + this.dnsOverrideJson = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProxyProfileCompanion.insert({ + required String id, + required String name, + required String type, + required String configJson, + this.dnsOverrideJson = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + type = Value(type), + configJson = Value(configJson); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? type, + Expression? configJson, + Expression? dnsOverrideJson, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (type != null) 'type': type, + if (configJson != null) 'config_json': configJson, + if (dnsOverrideJson != null) 'dns_override_json': dnsOverrideJson, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ProxyProfileCompanion copyWith({ + Value? id, + Value? name, + Value? type, + Value? configJson, + Value? dnsOverrideJson, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return ProxyProfileCompanion( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + configJson: configJson ?? this.configJson, + dnsOverrideJson: dnsOverrideJson ?? this.dnsOverrideJson, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (configJson.present) { + map['config_json'] = Variable(configJson.value); + } + if (dnsOverrideJson.present) { + map['dns_override_json'] = Variable(dnsOverrideJson.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProxyProfileCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('configJson: $configJson, ') + ..write('dnsOverrideJson: $dnsOverrideJson, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class IconCache extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + IconCache(this.attachedDatabase, [this._alias]); + late final GeneratedColumn origin = GeneratedColumn( + 'origin', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn iconData = + GeneratedColumn( + 'icon_data', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn fetchDate = GeneratedColumn( + 'fetch_date', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [origin, iconData, fetchDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'icon_cache'; + @override + Set get $primaryKey => {origin}; + @override + IconCacheData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return IconCacheData( + origin: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}origin'], + )!, + iconData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}icon_data'], + )!, + fetchDate: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}fetch_date'], + )!, + ); + } + + @override + IconCache createAlias(String alias) { + return IconCache(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class IconCacheData extends DataClass implements Insertable { + final String origin; + final i2.Uint8List iconData; + final int fetchDate; + const IconCacheData({ + required this.origin, + required this.iconData, + required this.fetchDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['origin'] = Variable(origin); + map['icon_data'] = Variable(iconData); + map['fetch_date'] = Variable(fetchDate); + return map; + } + + factory IconCacheData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return IconCacheData( + origin: serializer.fromJson(json['origin']), + iconData: serializer.fromJson(json['iconData']), + fetchDate: serializer.fromJson(json['fetchDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'origin': serializer.toJson(origin), + 'iconData': serializer.toJson(iconData), + 'fetchDate': serializer.toJson(fetchDate), + }; + } + + IconCacheData copyWith({ + String? origin, + i2.Uint8List? iconData, + int? fetchDate, + }) => IconCacheData( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + ); + IconCacheData copyWithCompanion(IconCacheCompanion data) { + return IconCacheData( + origin: data.origin.present ? data.origin.value : this.origin, + iconData: data.iconData.present ? data.iconData.value : this.iconData, + fetchDate: data.fetchDate.present ? data.fetchDate.value : this.fetchDate, + ); + } + + @override + String toString() { + return (StringBuffer('IconCacheData(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(origin, $driftBlobEquality.hash(iconData), fetchDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is IconCacheData && + other.origin == this.origin && + $driftBlobEquality.equals(other.iconData, this.iconData) && + other.fetchDate == this.fetchDate); +} + +class IconCacheCompanion extends UpdateCompanion { + final Value origin; + final Value iconData; + final Value fetchDate; + final Value rowid; + const IconCacheCompanion({ + this.origin = const Value.absent(), + this.iconData = const Value.absent(), + this.fetchDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + IconCacheCompanion.insert({ + required String origin, + required i2.Uint8List iconData, + required int fetchDate, + this.rowid = const Value.absent(), + }) : origin = Value(origin), + iconData = Value(iconData), + fetchDate = Value(fetchDate); + static Insertable custom({ + Expression? origin, + Expression? iconData, + Expression? fetchDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (origin != null) 'origin': origin, + if (iconData != null) 'icon_data': iconData, + if (fetchDate != null) 'fetch_date': fetchDate, + if (rowid != null) 'rowid': rowid, + }); + } + + IconCacheCompanion copyWith({ + Value? origin, + Value? iconData, + Value? fetchDate, + Value? rowid, + }) { + return IconCacheCompanion( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (origin.present) { + map['origin'] = Variable(origin.value); + } + if (iconData.present) { + map['icon_data'] = Variable(iconData.value); + } + if (fetchDate.present) { + map['fetch_date'] = Variable(fetchDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('IconCacheCompanion(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Onboarding extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Onboarding(this.attachedDatabase, [this._alias]); + late final GeneratedColumn revision = GeneratedColumn( + 'revision', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn completionDate = GeneratedColumn( + 'completion_date', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [revision, completionDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'onboarding'; + @override + Set get $primaryKey => const {}; + @override + OnboardingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OnboardingData( + revision: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}revision'], + )!, + completionDate: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}completion_date'], + )!, + ); + } + + @override + Onboarding createAlias(String alias) { + return Onboarding(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class OnboardingData extends DataClass implements Insertable { + final int revision; + final int completionDate; + const OnboardingData({required this.revision, required this.completionDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['revision'] = Variable(revision); + map['completion_date'] = Variable(completionDate); + return map; + } + + factory OnboardingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OnboardingData( + revision: serializer.fromJson(json['revision']), + completionDate: serializer.fromJson(json['completionDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'revision': serializer.toJson(revision), + 'completionDate': serializer.toJson(completionDate), + }; + } + + OnboardingData copyWith({int? revision, int? completionDate}) => + OnboardingData( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + ); + OnboardingData copyWithCompanion(OnboardingCompanion data) { + return OnboardingData( + revision: data.revision.present ? data.revision.value : this.revision, + completionDate: data.completionDate.present + ? data.completionDate.value + : this.completionDate, + ); + } + + @override + String toString() { + return (StringBuffer('OnboardingData(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(revision, completionDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OnboardingData && + other.revision == this.revision && + other.completionDate == this.completionDate); +} + +class OnboardingCompanion extends UpdateCompanion { + final Value revision; + final Value completionDate; + final Value rowid; + const OnboardingCompanion({ + this.revision = const Value.absent(), + this.completionDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + OnboardingCompanion.insert({ + required int revision, + required int completionDate, + this.rowid = const Value.absent(), + }) : revision = Value(revision), + completionDate = Value(completionDate); + static Insertable custom({ + Expression? revision, + Expression? completionDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (revision != null) 'revision': revision, + if (completionDate != null) 'completion_date': completionDate, + if (rowid != null) 'rowid': rowid, + }); + } + + OnboardingCompanion copyWith({ + Value? revision, + Value? completionDate, + Value? rowid, + }) { + return OnboardingCompanion( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (revision.present) { + map['revision'] = Variable(revision.value); + } + if (completionDate.present) { + map['completion_date'] = Variable(completionDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OnboardingCompanion(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Riverpod extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Riverpod(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn json = GeneratedColumn( + 'json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn expireAt = GeneratedColumn( + 'expireAt', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn destroyKey = GeneratedColumn( + 'destroyKey', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, json, expireAt, destroyKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'riverpod'; + @override + Set get $primaryKey => {key}; + @override + RiverpodData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RiverpodData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + json: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}json'], + )!, + expireAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}expireAt'], + ), + destroyKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}destroyKey'], + ), + ); + } + + @override + Riverpod createAlias(String alias) { + return Riverpod(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get dontWriteConstraints => true; +} + +class RiverpodData extends DataClass implements Insertable { + final String key; + final String json; + final int? expireAt; + final String? destroyKey; + const RiverpodData({ + required this.key, + required this.json, + this.expireAt, + this.destroyKey, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['json'] = Variable(json); + if (!nullToAbsent || expireAt != null) { + map['expireAt'] = Variable(expireAt); + } + if (!nullToAbsent || destroyKey != null) { + map['destroyKey'] = Variable(destroyKey); + } + return map; + } + + factory RiverpodData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RiverpodData( + key: serializer.fromJson(json['key']), + json: serializer.fromJson(json['json']), + expireAt: serializer.fromJson(json['expireAt']), + destroyKey: serializer.fromJson(json['destroyKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'json': serializer.toJson(json), + 'expireAt': serializer.toJson(expireAt), + 'destroyKey': serializer.toJson(destroyKey), + }; + } + + RiverpodData copyWith({ + String? key, + String? json, + Value expireAt = const Value.absent(), + Value destroyKey = const Value.absent(), + }) => RiverpodData( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt.present ? expireAt.value : this.expireAt, + destroyKey: destroyKey.present ? destroyKey.value : this.destroyKey, + ); + RiverpodData copyWithCompanion(RiverpodCompanion data) { + return RiverpodData( + key: data.key.present ? data.key.value : this.key, + json: data.json.present ? data.json.value : this.json, + expireAt: data.expireAt.present ? data.expireAt.value : this.expireAt, + destroyKey: data.destroyKey.present + ? data.destroyKey.value + : this.destroyKey, + ); + } + + @override + String toString() { + return (StringBuffer('RiverpodData(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, json, expireAt, destroyKey); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RiverpodData && + other.key == this.key && + other.json == this.json && + other.expireAt == this.expireAt && + other.destroyKey == this.destroyKey); +} + +class RiverpodCompanion extends UpdateCompanion { + final Value key; + final Value json; + final Value expireAt; + final Value destroyKey; + const RiverpodCompanion({ + this.key = const Value.absent(), + this.json = const Value.absent(), + this.expireAt = const Value.absent(), + this.destroyKey = const Value.absent(), + }); + RiverpodCompanion.insert({ + required String key, + required String json, + this.expireAt = const Value.absent(), + this.destroyKey = const Value.absent(), + }) : key = Value(key), + json = Value(json); + static Insertable custom({ + Expression? key, + Expression? json, + Expression? expireAt, + Expression? destroyKey, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (json != null) 'json': json, + if (expireAt != null) 'expireAt': expireAt, + if (destroyKey != null) 'destroyKey': destroyKey, + }); + } + + RiverpodCompanion copyWith({ + Value? key, + Value? json, + Value? expireAt, + Value? destroyKey, + }) { + return RiverpodCompanion( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt ?? this.expireAt, + destroyKey: destroyKey ?? this.destroyKey, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (json.present) { + map['json'] = Variable(json.value); + } + if (expireAt.present) { + map['expireAt'] = Variable(expireAt.value); + } + if (destroyKey.present) { + map['destroyKey'] = Variable(destroyKey.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RiverpodCompanion(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } +} + +class ToolbarButtonConfigs extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ToolbarButtonConfigs(this.attachedDatabase, [this._alias]); + late final GeneratedColumn buttonId = GeneratedColumn( + 'button_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY', + ); + late final GeneratedColumn orderKey = GeneratedColumn( + 'order_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isVisible = GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT TRUE', + defaultValue: const CustomExpression('TRUE'), + ); + late final GeneratedColumn fallbackId = GeneratedColumn( + 'fallback_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL', + ); + @override + List get $columns => [ + buttonId, + orderKey, + isVisible, + fallbackId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'toolbar_button_configs'; + @override + Set get $primaryKey => {buttonId}; + @override + ToolbarButtonConfigsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ToolbarButtonConfigsData( + buttonId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}button_id'], + )!, + orderKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_key'], + )!, + isVisible: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_visible'], + )!, + fallbackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}fallback_id'], + ), + ); + } + + @override + ToolbarButtonConfigs createAlias(String alias) { + return ToolbarButtonConfigs(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ToolbarButtonConfigsData extends DataClass + implements Insertable { + final String buttonId; + final String orderKey; + final int isVisible; + final String? fallbackId; + const ToolbarButtonConfigsData({ + required this.buttonId, + required this.orderKey, + required this.isVisible, + this.fallbackId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['button_id'] = Variable(buttonId); + map['order_key'] = Variable(orderKey); + map['is_visible'] = Variable(isVisible); + if (!nullToAbsent || fallbackId != null) { + map['fallback_id'] = Variable(fallbackId); + } + return map; + } + + factory ToolbarButtonConfigsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ToolbarButtonConfigsData( + buttonId: serializer.fromJson(json['buttonId']), + orderKey: serializer.fromJson(json['orderKey']), + isVisible: serializer.fromJson(json['isVisible']), + fallbackId: serializer.fromJson(json['fallbackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'buttonId': serializer.toJson(buttonId), + 'orderKey': serializer.toJson(orderKey), + 'isVisible': serializer.toJson(isVisible), + 'fallbackId': serializer.toJson(fallbackId), + }; + } + + ToolbarButtonConfigsData copyWith({ + String? buttonId, + String? orderKey, + int? isVisible, + Value fallbackId = const Value.absent(), + }) => ToolbarButtonConfigsData( + buttonId: buttonId ?? this.buttonId, + orderKey: orderKey ?? this.orderKey, + isVisible: isVisible ?? this.isVisible, + fallbackId: fallbackId.present ? fallbackId.value : this.fallbackId, + ); + ToolbarButtonConfigsData copyWithCompanion( + ToolbarButtonConfigsCompanion data, + ) { + return ToolbarButtonConfigsData( + buttonId: data.buttonId.present ? data.buttonId.value : this.buttonId, + orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + fallbackId: data.fallbackId.present + ? data.fallbackId.value + : this.fallbackId, + ); + } + + @override + String toString() { + return (StringBuffer('ToolbarButtonConfigsData(') + ..write('buttonId: $buttonId, ') + ..write('orderKey: $orderKey, ') + ..write('isVisible: $isVisible, ') + ..write('fallbackId: $fallbackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(buttonId, orderKey, isVisible, fallbackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ToolbarButtonConfigsData && + other.buttonId == this.buttonId && + other.orderKey == this.orderKey && + other.isVisible == this.isVisible && + other.fallbackId == this.fallbackId); +} + +class ToolbarButtonConfigsCompanion + extends UpdateCompanion { + final Value buttonId; + final Value orderKey; + final Value isVisible; + final Value fallbackId; + final Value rowid; + const ToolbarButtonConfigsCompanion({ + this.buttonId = const Value.absent(), + this.orderKey = const Value.absent(), + this.isVisible = const Value.absent(), + this.fallbackId = const Value.absent(), + this.rowid = const Value.absent(), + }); + ToolbarButtonConfigsCompanion.insert({ + required String buttonId, + required String orderKey, + this.isVisible = const Value.absent(), + this.fallbackId = const Value.absent(), + this.rowid = const Value.absent(), + }) : buttonId = Value(buttonId), + orderKey = Value(orderKey); + static Insertable custom({ + Expression? buttonId, + Expression? orderKey, + Expression? isVisible, + Expression? fallbackId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (buttonId != null) 'button_id': buttonId, + if (orderKey != null) 'order_key': orderKey, + if (isVisible != null) 'is_visible': isVisible, + if (fallbackId != null) 'fallback_id': fallbackId, + if (rowid != null) 'rowid': rowid, + }); + } + + ToolbarButtonConfigsCompanion copyWith({ + Value? buttonId, + Value? orderKey, + Value? isVisible, + Value? fallbackId, + Value? rowid, + }) { + return ToolbarButtonConfigsCompanion( + buttonId: buttonId ?? this.buttonId, + orderKey: orderKey ?? this.orderKey, + isVisible: isVisible ?? this.isVisible, + fallbackId: fallbackId ?? this.fallbackId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (buttonId.present) { + map['button_id'] = Variable(buttonId.value); + } + if (orderKey.present) { + map['order_key'] = Variable(orderKey.value); + } + if (isVisible.present) { + map['is_visible'] = Variable(isVisible.value); + } + if (fallbackId.present) { + map['fallback_id'] = Variable(fallbackId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ToolbarButtonConfigsCompanion(') + ..write('buttonId: $buttonId, ') + ..write('orderKey: $orderKey, ') + ..write('isVisible: $isVisible, ') + ..write('fallbackId: $fallbackId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SearchTokens extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SearchTokens(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn token = + GeneratedColumn( + 'token', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn insertedAt = GeneratedColumn( + 'inserted_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn issuerKeyVersion = GeneratedColumn( + 'issuer_key_version', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn reservedAt = GeneratedColumn( + 'reserved_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [ + id, + token, + insertedAt, + issuerKeyVersion, + reservedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'search_tokens'; + @override + Set get $primaryKey => {id}; + @override + SearchTokensData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SearchTokensData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + token: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}token'], + )!, + insertedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}inserted_at'], + )!, + issuerKeyVersion: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}issuer_key_version'], + )!, + reservedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}reserved_at'], + ), + ); + } + + @override + SearchTokens createAlias(String alias) { + return SearchTokens(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class SearchTokensData extends DataClass + implements Insertable { + final int id; + final i2.Uint8List token; + final int insertedAt; + final String issuerKeyVersion; + final int? reservedAt; + const SearchTokensData({ + required this.id, + required this.token, + required this.insertedAt, + required this.issuerKeyVersion, + this.reservedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['token'] = Variable(token); + map['inserted_at'] = Variable(insertedAt); + map['issuer_key_version'] = Variable(issuerKeyVersion); + if (!nullToAbsent || reservedAt != null) { + map['reserved_at'] = Variable(reservedAt); + } + return map; + } + + factory SearchTokensData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SearchTokensData( + id: serializer.fromJson(json['id']), + token: serializer.fromJson(json['token']), + insertedAt: serializer.fromJson(json['insertedAt']), + issuerKeyVersion: serializer.fromJson(json['issuerKeyVersion']), + reservedAt: serializer.fromJson(json['reservedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'token': serializer.toJson(token), + 'insertedAt': serializer.toJson(insertedAt), + 'issuerKeyVersion': serializer.toJson(issuerKeyVersion), + 'reservedAt': serializer.toJson(reservedAt), + }; + } + + SearchTokensData copyWith({ + int? id, + i2.Uint8List? token, + int? insertedAt, + String? issuerKeyVersion, + Value reservedAt = const Value.absent(), + }) => SearchTokensData( + id: id ?? this.id, + token: token ?? this.token, + insertedAt: insertedAt ?? this.insertedAt, + issuerKeyVersion: issuerKeyVersion ?? this.issuerKeyVersion, + reservedAt: reservedAt.present ? reservedAt.value : this.reservedAt, + ); + SearchTokensData copyWithCompanion(SearchTokensCompanion data) { + return SearchTokensData( + id: data.id.present ? data.id.value : this.id, + token: data.token.present ? data.token.value : this.token, + insertedAt: data.insertedAt.present + ? data.insertedAt.value + : this.insertedAt, + issuerKeyVersion: data.issuerKeyVersion.present + ? data.issuerKeyVersion.value + : this.issuerKeyVersion, + reservedAt: data.reservedAt.present + ? data.reservedAt.value + : this.reservedAt, + ); + } + + @override + String toString() { + return (StringBuffer('SearchTokensData(') + ..write('id: $id, ') + ..write('token: $token, ') + ..write('insertedAt: $insertedAt, ') + ..write('issuerKeyVersion: $issuerKeyVersion, ') + ..write('reservedAt: $reservedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + $driftBlobEquality.hash(token), + insertedAt, + issuerKeyVersion, + reservedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SearchTokensData && + other.id == this.id && + $driftBlobEquality.equals(other.token, this.token) && + other.insertedAt == this.insertedAt && + other.issuerKeyVersion == this.issuerKeyVersion && + other.reservedAt == this.reservedAt); +} + +class SearchTokensCompanion extends UpdateCompanion { + final Value id; + final Value token; + final Value insertedAt; + final Value issuerKeyVersion; + final Value reservedAt; + const SearchTokensCompanion({ + this.id = const Value.absent(), + this.token = const Value.absent(), + this.insertedAt = const Value.absent(), + this.issuerKeyVersion = const Value.absent(), + this.reservedAt = const Value.absent(), + }); + SearchTokensCompanion.insert({ + this.id = const Value.absent(), + required i2.Uint8List token, + required int insertedAt, + required String issuerKeyVersion, + this.reservedAt = const Value.absent(), + }) : token = Value(token), + insertedAt = Value(insertedAt), + issuerKeyVersion = Value(issuerKeyVersion); + static Insertable custom({ + Expression? id, + Expression? token, + Expression? insertedAt, + Expression? issuerKeyVersion, + Expression? reservedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (token != null) 'token': token, + if (insertedAt != null) 'inserted_at': insertedAt, + if (issuerKeyVersion != null) 'issuer_key_version': issuerKeyVersion, + if (reservedAt != null) 'reserved_at': reservedAt, + }); + } + + SearchTokensCompanion copyWith({ + Value? id, + Value? token, + Value? insertedAt, + Value? issuerKeyVersion, + Value? reservedAt, + }) { + return SearchTokensCompanion( + id: id ?? this.id, + token: token ?? this.token, + insertedAt: insertedAt ?? this.insertedAt, + issuerKeyVersion: issuerKeyVersion ?? this.issuerKeyVersion, + reservedAt: reservedAt ?? this.reservedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (token.present) { + map['token'] = Variable(token.value); + } + if (insertedAt.present) { + map['inserted_at'] = Variable(insertedAt.value); + } + if (issuerKeyVersion.present) { + map['issuer_key_version'] = Variable(issuerKeyVersion.value); + } + if (reservedAt.present) { + map['reserved_at'] = Variable(reservedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SearchTokensCompanion(') + ..write('id: $id, ') + ..write('token: $token, ') + ..write('insertedAt: $insertedAt, ') + ..write('issuerKeyVersion: $issuerKeyVersion, ') + ..write('reservedAt: $reservedAt') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV8 extends GeneratedDatabase { + DatabaseAtV8(QueryExecutor e) : super(e); + late final Setting setting = Setting(this); + late final ProxyProfile proxyProfile = ProxyProfile(this); + late final Index idxProxyProfileUpdatedAt = Index( + 'idx_proxy_profile_updated_at', + 'CREATE INDEX idx_proxy_profile_updated_at ON proxy_profile (updated_at)', + ); + late final IconCache iconCache = IconCache(this); + late final Onboarding onboarding = Onboarding(this); + late final Riverpod riverpod = Riverpod(this); + late final ToolbarButtonConfigs toolbarButtonConfigs = ToolbarButtonConfigs( + this, + ); + late final Index idxToolbarOrderKey = Index( + 'idx_toolbar_order_key', + 'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)', + ); + late final SearchTokens searchTokens = SearchTokens(this); + late final Index idxSearchTokensInsertedAt = Index( + 'idx_search_tokens_inserted_at', + 'CREATE INDEX idx_search_tokens_inserted_at ON search_tokens (inserted_at)', + ); + late final Index idxSearchTokensReservedAt = Index( + 'idx_search_tokens_reserved_at', + 'CREATE INDEX idx_search_tokens_reserved_at ON search_tokens (reserved_at)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + setting, + proxyProfile, + idxProxyProfileUpdatedAt, + iconCache, + onboarding, + riverpod, + toolbarButtonConfigs, + idxToolbarOrderKey, + searchTokens, + idxSearchTokensInsertedAt, + idxSearchTokensReservedAt, + ]; + @override + int get schemaVersion => 8; +} diff --git a/apps/weblibre/test/features/geckoview/features/browser/domain/services/proxy_settings_replication_test.dart b/apps/weblibre/test/features/geckoview/features/browser/domain/services/proxy_settings_replication_test.dart new file mode 100644 index 00000000..6a428060 --- /dev/null +++ b/apps/weblibre/test/features/geckoview/features/browser/domain/services/proxy_settings_replication_test.dart @@ -0,0 +1,174 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:drift/native.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_tor/flutter_tor.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/data/database/functions/lexo_rank_functions.dart'; +import 'package:weblibre/data/database/functions/url_functions.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; +import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test( + 'container assignments ensure sing-box profiles are running before syncing', + () async { + final profileId = SingboxProxyConnectionId('profile-1'); + final assignedContainer = _container( + id: 'container-1', + contextId: 'context-a', + proxyConnectionId: profileId, + ); + final db = TabDatabase( + NativeDatabase.memory( + setup: (database) { + registerLexorankFunctions(database); + registerUrlFunctions(database); + }, + ), + ); + final containerProxyRepository = _FakeContainerProxyRepository(); + final containerRepository = _FakeContainerRepository([assignedContainer]); + final runtimeRepository = _FakeSingboxProxyRuntimeRepository(); + final container = ProviderContainer( + overrides: [ + tabDatabaseProvider.overrideWith((ref) => db), + containerProxyRepositoryProvider.overrideWith( + () => containerProxyRepository, + ), + containerRepositoryProvider.overrideWith(() => containerRepository), + singboxProxyRuntimeRepositoryProvider.overrideWith( + () => runtimeRepository, + ), + torProxyServiceProvider.overrideWith(_FakeTorProxyService.new), + proxyRoutingSettingsWithDefaultsProvider.overrideWith( + (ref) => ProxyRoutingSettings.withDefaults(), + ), + watchContainersWithCountProvider.overrideWith( + (ref) => Stream.value([assignedContainer]), + ), + watchAllAssignedSitesProvider.overrideWith( + (ref) => Stream.value(const []), + ), + watchIsolatedContextContainerMapProvider.overrideWith( + (ref) => Stream.value(const >{}), + ), + ], + ); + addTearDown(() async { + container.dispose(); + await db.close(); + }); + + final subscription = container.listen( + proxySettingsReplicationProvider, + (previous, next) {}, + fireImmediately: true, + ); + addTearDown(subscription.close); + await pumpEventQueue(); + + expect(runtimeRepository.ensuredProxyConnectionIds, [profileId]); + expect(containerProxyRepository.setContainerProxyCalls, [ + ('context-a', profileId.encode()), + ]); + }, + ); +} + +ContainerDataWithCount _container({ + required String id, + required String contextId, + required ProxyConnectionId proxyConnectionId, +}) { + return ContainerDataWithCount( + id: id, + name: 'Container', + color: const Color(0xFF336699), + orderKey: 'a', + metadata: ContainerMetadata.withDefaults( + contextualIdentity: contextId, + proxyConnectionId: proxyConnectionId, + ), + tabCount: 0, + ); +} + +class _FakeContainerProxyRepository extends ContainerProxyRepository { + final setContainerProxyCalls = <(String, String)>[]; + + @override + Future setTorProxyPort(int? port) async {} + + @override + Future setSiteAssignments(List assignements) async {} + + @override + Future setContainerProxy(String contextId, String proxyId) async { + setContainerProxyCalls.add((contextId, proxyId)); + } + + @override + Future clearContainerProxy(String contextId) async {} + + @override + void build() {} +} + +class _FakeContainerRepository extends ContainerRepository { + final List containers; + + _FakeContainerRepository(this.containers); + + @override + Future> getAllContainersWithCount() async { + return containers; + } + + @override + void build() {} +} + +class _FakeSingboxProxyRuntimeRepository extends SingboxProxyRuntimeRepository { + final ensuredProxyConnectionIds = []; + + @override + Future ensureProxyConnectionAvailable( + SingboxProxyConnectionId connectionId, + ) async { + ensuredProxyConnectionIds.add(connectionId); + } + + @override + Future build() async { + return SingboxProxyRuntimeState( + status: SingboxProxyRuntimeStatus.stopped, + endpoints: [], + ); + } +} + +class _FakeTorProxyService extends TorProxyService { + @override + Stream build() async* { + // Mirror production: the real TorProxyService seeds the provider with the + // current status so downstream `selectAsync` paths don't stay in + // AsyncLoading forever. Tor is stopped in these tests. + yield TorStatus(isRunning: false, bootstrapProgress: 0); + } +} diff --git a/apps/weblibre/test/features/geckoview/features/tabs/data/models/container_data_test.dart b/apps/weblibre/test/features/geckoview/features/tabs/data/models/container_data_test.dart new file mode 100644 index 00000000..0b621876 --- /dev/null +++ b/apps/weblibre/test/features/geckoview/features/tabs/data/models/container_data_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; + +void main() { + group('ContainerMetadata proxy serialization', () { + test('round-trips proxyConnectionId through JSON', () { + final metadata = ContainerMetadata.withDefaults( + proxyConnectionId: const SingboxProxyConnectionId('profile-1'), + clearDataOnExit: true, + ); + + final json = metadata.toJson(); + final restored = ContainerMetadata.fromJson(json); + + expect( + json['proxyConnectionId'], + const SingboxProxyConnectionId('profile-1').encode(), + ); + expect(json['clearDataOnExit'], isTrue); + expect(restored.proxyConnectionId, metadata.proxyConnectionId); + expect(restored.usesTorProxy, isFalse); + }); + + test('usesTorProxy is true only for the Tor connection id', () { + final tor = ContainerMetadata.withDefaults( + proxyConnectionId: const TorProxyConnectionId(), + ); + + expect(tor.usesTorProxy, isTrue); + }); + }); +} diff --git a/apps/weblibre/test/features/geckoview/features/tabs/presentation/screens/container_edit_test.dart b/apps/weblibre/test/features/geckoview/features/tabs/presentation/screens/container_edit_test.dart new file mode 100644 index 00000000..ccabd60f --- /dev/null +++ b/apps/weblibre/test/features/geckoview/features/tabs/presentation/screens/container_edit_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_edit.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart'; + +void main() { + testWidgets( + 'clearing proxy selection does not leave cookie isolation enabled in create mode', + (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + proxyConnectionOptionsProvider.overrideWith( + (ref) => const [], + ), + ], + child: MaterialApp( + home: ContainerEditScreen.create( + initialContainer: ContainerData( + id: 'container-1', + color: Colors.blue, + orderKey: 'a', + metadata: ContainerMetadata.withDefaults( + proxyConnectionId: SingboxProxyConnectionId('missing-proxy'), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Proxy Connection')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Clear')); + await tester.pumpAndSettle(); + + final cookieIsolationTile = tester.widget( + find.widgetWithText(SwitchListTile, 'Cookie Isolation'), + ); + + expect(cookieIsolationTile.value, isFalse); + expect(find.text('None'), findsOneWidget); + }, + ); + + testWidgets( + 'dismissing proxy picker does not leave cookie isolation enabled in create mode', + (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + proxyConnectionOptionsProvider.overrideWith( + (ref) => const [], + ), + ], + child: MaterialApp( + home: ContainerEditScreen.create( + initialContainer: ContainerData( + id: 'container-1', + color: Colors.blue, + orderKey: 'a', + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Proxy Connection')); + await tester.pumpAndSettle(); + await tester.tapAt(const Offset(8, 8)); + await tester.pumpAndSettle(); + + final cookieIsolationTile = tester.widget( + find.widgetWithText(SwitchListTile, 'Cookie Isolation'), + ); + + expect(cookieIsolationTile.value, isFalse); + expect(find.text('New Container'), findsOneWidget); + }, + ); +} diff --git a/apps/weblibre/test/features/proxy/data/models/singbox_proxy_form_test.dart b/apps/weblibre/test/features/proxy/data/models/singbox_proxy_form_test.dart new file mode 100644 index 00000000..62863cd8 --- /dev/null +++ b/apps/weblibre/test/features/proxy/data/models/singbox_proxy_form_test.dart @@ -0,0 +1,345 @@ +import 'dart:convert'; + +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:weblibre/features/proxy/data/forms/singbox_form_specs.dart'; +import 'package:weblibre/features/proxy/data/parsers/singbox_proxy_uri.dart'; + +void main() { + group('SingboxProxyFormSpec', () { + test('builds public and secret JSON for Shadowsocks', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.shadowsocks]!; + final values = { + 'server': 'ss.example.com', + 'server_port': '8388', + 'method': '2022-blake3-aes-128-gcm', + 'password': 'secret', + }; + + expect(spec.validate(values), isNull); + + final config = jsonDecode(spec.toConfigJson(values)); + final secrets = jsonDecode(spec.toSecretJson(values)!); + + expect(config, { + 'type': 'shadowsocks', + 'server': 'ss.example.com', + 'server_port': 8388, + 'method': '2022-blake3-aes-128-gcm', + }); + expect(secrets, {'password': 'secret'}); + }); + + test('hydrates structured values from public and secret JSON', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.vless]!; + + final values = spec.valuesFromJson( + configJson: ''' + { + "type": "vless", + "server": "vless.example.com", + "server_port": 443, + "flow": "xtls-rprx-vision" + }''', + secretJson: '{"uuid":"00000000-0000-0000-0000-000000000000"}', + ); + + expect(values['server'], 'vless.example.com'); + expect(values['server_port'], '443'); + expect(values['uuid'], '00000000-0000-0000-0000-000000000000'); + expect(values['flow'], 'xtls-rprx-vision'); + }); + + test('rejects invalid server ports', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.http]!; + + expect( + spec.validate({'server': 'proxy.example.com', 'server_port': '70000'}), + 'Server Port must be between 1 and 65535.', + ); + }); + + test('builds nested advanced config fields', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.vless]!; + final values = { + 'server': 'vless.example.com', + 'server_port': '443', + 'uuid': '00000000-0000-0000-0000-000000000000', + 'flow': 'xtls-rprx-vision', + 'tls.enabled': 'true', + 'tls.server_name': 'front.example.com', + 'tls.alpn': 'h2, http/1.1', + 'transport.type': 'ws', + 'transport.path': '/proxy', + 'multiplex.enabled': 'true', + 'multiplex.max_connections': '4', + 'domain_strategy': 'prefer_ipv4', + }; + + expect(spec.validate(values), isNull); + + final config = + jsonDecode(spec.toConfigJson(values)) as Map; + final secrets = + jsonDecode(spec.toSecretJson(values)!) as Map; + + expect(config['tls'], { + 'enabled': true, + 'server_name': 'front.example.com', + 'alpn': ['h2', 'http/1.1'], + }); + expect(config['transport'], {'type': 'ws', 'path': '/proxy'}); + expect(config['multiplex'], {'enabled': true, 'max_connections': 4}); + expect(config['domain_strategy'], 'prefer_ipv4'); + expect(secrets, {'uuid': '00000000-0000-0000-0000-000000000000'}); + }); + + test('builds public and secret JSON for WireGuard', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.wireguard]!; + final values = { + 'server': 'wg.example.com', + 'server_port': '51820', + 'local_address': '10.0.0.2/32\nfd00::2/128', + 'private_key': 'private-key', + 'peer_public_key': 'peer-public-key', + 'pre_shared_key': 'pre-shared-key', + 'mtu': '1420', + 'reserved': '1, 2, 3', + }; + + expect(spec.validate(values), isNull); + + final config = jsonDecode(spec.toConfigJson(values)); + final secrets = jsonDecode(spec.toSecretJson(values)!); + + expect(config, { + 'type': 'wireguard', + 'server': 'wg.example.com', + 'server_port': 51820, + 'local_address': ['10.0.0.2/32', 'fd00::2/128'], + 'peer_public_key': 'peer-public-key', + 'mtu': 1420, + 'reserved': [1, 2, 3], + }); + expect(secrets, { + 'private_key': 'private-key', + 'pre_shared_key': 'pre-shared-key', + }); + }); + + test('hydrates WireGuard values from public and secret JSON', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.wireguard]!; + + final values = spec.valuesFromJson( + configJson: ''' + { + "type": "wireguard", + "server": "wg.example.com", + "server_port": 51820, + "local_address": ["10.0.0.2/32"], + "peer_public_key": "peer-public-key", + "mtu": 1420, + "reserved": [1, 2, 3] + }''', + secretJson: ''' + { + "private_key": "private-key", + "pre_shared_key": "pre-shared-key" + }''', + ); + + expect(values['server'], 'wg.example.com'); + expect(values['server_port'], '51820'); + expect(values['local_address'], '10.0.0.2/32'); + expect(values['private_key'], 'private-key'); + expect(values['peer_public_key'], 'peer-public-key'); + expect(values['pre_shared_key'], 'pre-shared-key'); + expect(values['mtu'], '1420'); + expect(values['reserved'], '1\n2\n3'); + }); + + test('rejects invalid WireGuard reserved bytes', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.wireguard]!; + + expect( + spec.validate({ + 'server': 'wg.example.com', + 'server_port': '51820', + 'local_address': '10.0.0.2/32', + 'private_key': 'private-key', + 'peer_public_key': 'peer-public-key', + 'mtu': '1420', + 'reserved': '1, 2', + }), + 'Reserved Bytes must contain 3 numbers.', + ); + }); + }); + + group('importSingboxProxyUri', () { + test('imports Shadowsocks SIP002 URI', () { + final credentials = base64Url.encode( + utf8.encode('aes-256-gcm:secret-password'), + ); + + final imported = importSingboxProxyUri( + 'ss://$credentials@ss.example.com:8388#Work%20SS', + ); + + expect(imported.type, SingboxProxyProfileType.shadowsocks); + expect(imported.name, 'Work SS'); + expect(imported.values, { + 'server': 'ss.example.com', + 'server_port': '8388', + 'method': 'aes-256-gcm', + 'password': 'secret-password', + }); + }); + + test('imports legacy base64 Shadowsocks URI', () { + final payload = base64Url.encode( + utf8.encode('chacha20-ietf-poly1305:secret@[2001:db8::1]:8388'), + ); + + final imported = importSingboxProxyUri('ss://$payload'); + + expect(imported.type, SingboxProxyProfileType.shadowsocks); + expect(imported.values['server'], '2001:db8::1'); + expect(imported.values['server_port'], '8388'); + expect(imported.values['method'], 'chacha20-ietf-poly1305'); + expect(imported.values['password'], 'secret'); + }); + + test('imports Trojan URI', () { + final imported = importSingboxProxyUri( + 'trojan://secret%20password@trojan.example.com:443#Trojan', + ); + + expect(imported.type, SingboxProxyProfileType.trojan); + expect(imported.name, 'Trojan'); + expect(imported.values, { + 'server': 'trojan.example.com', + 'server_port': '443', + 'password': 'secret password', + 'tls.enabled': 'true', + }); + }); + + test('imports VLESS URI', () { + final imported = importSingboxProxyUri( + 'vless://00000000-0000-0000-0000-000000000000@vless.example.com:443?flow=xtls-rprx-vision#VLESS', + ); + + expect(imported.type, SingboxProxyProfileType.vless); + expect(imported.name, 'VLESS'); + expect(imported.values, { + 'server': 'vless.example.com', + 'server_port': '443', + 'uuid': '00000000-0000-0000-0000-000000000000', + 'flow': 'xtls-rprx-vision', + }); + }); + + test('imports VMess URI', () { + final payload = base64Url.encode( + utf8.encode( + jsonEncode({ + 'v': '2', + 'ps': 'VMess', + 'add': 'vmess.example.com', + 'port': '443', + 'id': '00000000-0000-0000-0000-000000000000', + 'aid': '0', + 'scy': 'auto', + }), + ), + ); + + final imported = importSingboxProxyUri('vmess://$payload'); + + expect(imported.type, SingboxProxyProfileType.vmess); + expect(imported.name, 'VMess'); + expect(imported.values, { + 'server': 'vmess.example.com', + 'server_port': '443', + 'uuid': '00000000-0000-0000-0000-000000000000', + 'security': 'auto', + 'alter_id': '0', + 'tls.enabled': '', + 'tls.server_name': '', + 'transport.type': '', + 'transport.path': '', + }); + }); + + test('imports SOCKS URI', () { + final imported = importSingboxProxyUri( + 'socks://user:secret@socks.example.com:1080#SOCKS', + ); + + expect(imported.type, SingboxProxyProfileType.socks); + expect(imported.name, 'SOCKS'); + expect(imported.values, { + 'server': 'socks.example.com', + 'server_port': '1080', + 'version': '5', + 'username': 'user', + 'password': 'secret', + }); + }); + + test('imports HTTPS proxy URI', () { + final imported = importSingboxProxyUri( + 'https://user:secret@proxy.example.com:443?sni=proxy.example.com#HTTP', + ); + + expect(imported.type, SingboxProxyProfileType.http); + expect(imported.values['server'], 'proxy.example.com'); + expect(imported.values['server_port'], '443'); + expect(imported.values['username'], 'user'); + expect(imported.values['password'], 'secret'); + expect(imported.values['tls.enabled'], 'true'); + expect(imported.values['tls.server_name'], 'proxy.example.com'); + }); + + test('imports Hysteria2 URI', () { + final imported = importSingboxProxyUri( + 'hy2://secret@hy2.example.com:443?obfs=salamander&obfs-password=obfs-secret&sni=front.example.com#HY2', + ); + + expect(imported.type, SingboxProxyProfileType.hysteria2); + expect(imported.name, 'HY2'); + expect(imported.values['server'], 'hy2.example.com'); + expect(imported.values['server_port'], '443'); + expect(imported.values['password'], 'secret'); + expect(imported.values['obfs.type'], 'salamander'); + expect(imported.values['obfs.password'], 'obfs-secret'); + expect(imported.values['tls.enabled'], 'true'); + expect(imported.values['tls.server_name'], 'front.example.com'); + }); + + test('imports TUIC URI', () { + final imported = importSingboxProxyUri( + 'tuic://00000000-0000-0000-0000-000000000000:secret@tuic.example.com:443?congestion_control=bbr&udp_relay_mode=quic&sni=front.example.com#TUIC', + ); + + expect(imported.type, SingboxProxyProfileType.tuic); + expect(imported.name, 'TUIC'); + expect(imported.values['server'], 'tuic.example.com'); + expect(imported.values['server_port'], '443'); + expect(imported.values['uuid'], '00000000-0000-0000-0000-000000000000'); + expect(imported.values['password'], 'secret'); + expect(imported.values['congestion_control'], 'bbr'); + expect(imported.values['udp_relay_mode'], 'quic'); + expect(imported.values['tls.enabled'], 'true'); + expect(imported.values['tls.server_name'], 'front.example.com'); + }); + + test('rejects unsupported URI schemes', () { + expect( + () => importSingboxProxyUri('hysteria2://example.com:443'), + throwsFormatException, + ); + }); + }); +} diff --git a/apps/weblibre/test/features/proxy/data/models/wireguard_config_import_test.dart b/apps/weblibre/test/features/proxy/data/models/wireguard_config_import_test.dart new file mode 100644 index 00000000..392d0239 --- /dev/null +++ b/apps/weblibre/test/features/proxy/data/models/wireguard_config_import_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:weblibre/features/proxy/data/models/wireguard_config_import.dart'; + +void main() { + group('WireguardConfigImport', () { + test('imports standard WireGuard config text', () { + final imported = WireguardConfigImport.fromConfigText(''' + [Interface] + PrivateKey = private-key + Address = 10.0.0.2/32, fd00::2/128 + MTU = 1420 + DNS = 1.1.1.1 + + [Peer] + PublicKey = peer-public-key + PresharedKey = pre-shared-key + AllowedIPs = 0.0.0.0/0, ::/0 + Endpoint = wg.example.com:51820 + '''); + + expect(imported.values, { + 'server': 'wg.example.com', + 'server_port': '51820', + 'local_address': '10.0.0.2/32\nfd00::2/128', + 'private_key': 'private-key', + 'peer_public_key': 'peer-public-key', + 'pre_shared_key': 'pre-shared-key', + 'mtu': '1420', + }); + expect(imported.primaryDnsAddress, 'udp://1.1.1.1'); + }); + + test('imports bracketed IPv6 WireGuard endpoint', () { + final imported = WireguardConfigImport.fromConfigText(''' + [Interface] + PrivateKey = private-key + Address = fd00::2/128 + + [Peer] + PublicKey = peer-public-key + Endpoint = [2001:db8::1]:51820 + '''); + + expect(imported.values['server'], '2001:db8::1'); + expect(imported.values['server_port'], '51820'); + expect(imported.values['mtu'], '1408'); + }); + + test('rejects configs without interface and peer sections', () { + expect( + () => WireguardConfigImport.fromConfigText('PrivateKey = private-key'), + throwsFormatException, + ); + }); + }); +} diff --git a/apps/weblibre/test/features/proxy/domain/providers/proxy_connection_options_test.dart b/apps/weblibre/test/features/proxy/domain/providers/proxy_connection_options_test.dart new file mode 100644 index 00000000..d5df07a1 --- /dev/null +++ b/apps/weblibre/test/features/proxy/domain/providers/proxy_connection_options_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; + +void main() { + group('proxyConnectionOptionsProvider', () { + test('returns Tor and saved sing-box profiles', () async { + final createdAt = DateTime(2026); + final container = ProviderContainer( + overrides: [ + singboxProxyProfilesRepositoryProvider.overrideWith( + () => _FakeProfilesRepository([ + ProxyProfile( + id: 'profile-1', + name: 'Mullvad', + type: SingboxProxyProfileType.customOutbound, + configJson: '{"type":"socks"}', + createdAt: createdAt, + updatedAt: createdAt, + ), + ]), + ), + ], + ); + addTearDown(container.dispose); + + container.listen( + singboxProxyProfilesRepositoryProvider, + (_, _) {}, + fireImmediately: true, + ); + await Future.delayed(Duration.zero); + final options = container.read(proxyConnectionOptionsProvider); + + expect(options.map((option) => option.id), [ + const TorProxyConnectionId(), + const SingboxProxyConnectionId('profile-1'), + ]); + expect(options.map((option) => option.title), ['Tor', 'Mullvad']); + }); + + test('labels unknown proxy ids explicitly', () { + expect( + proxyConnectionTitle( + const [], + const SingboxProxyConnectionId('missing'), + ), + 'Unknown proxy', + ); + }); + }); +} + +class _FakeProfilesRepository extends SingboxProxyProfilesRepository { + final List profiles; + + _FakeProfilesRepository(this.profiles); + + @override + Stream> build() => Stream.value(profiles); +} diff --git a/apps/weblibre/test/features/proxy/domain/repositories/singbox_proxy_profiles_test.dart b/apps/weblibre/test/features/proxy/domain/repositories/singbox_proxy_profiles_test.dart new file mode 100644 index 00000000..fdbe59d2 --- /dev/null +++ b/apps/weblibre/test/features/proxy/domain/repositories/singbox_proxy_profiles_test.dart @@ -0,0 +1,155 @@ +import 'package:drift/native.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/data/database/functions/lexo_rank_functions.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_credentials.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/user/data/database/database.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; +import 'package:weblibre/features/user/data/providers.dart'; + +void main() { + test( + 'createProfile persists metadata and writes secrets separately', + () async { + final credentials = _FakeCredentialsRepository(); + final container = _container(credentials); + addTearDown(container.dispose); + + final repository = container.read( + singboxProxyProfilesRepositoryProvider.notifier, + ); + final created = await repository.createProfile( + name: 'Mullvad WG', + type: SingboxProxyProfileType.wireguard, + configJson: '{"server":"vpn.example"}', + secretJson: '{"private_key":"secret"}', + ); + + final profiles = await repository.fetchProfiles(); + + expect(profiles, hasLength(1)); + expect(profiles.single.id, created.id); + expect(profiles.single.name, 'Mullvad WG'); + expect(profiles.single.type, SingboxProxyProfileType.wireguard); + expect(profiles.single.configJson, '{"server":"vpn.example"}'); + expect(credentials.writtenSecrets, { + created.id: '{"private_key":"secret"}', + }); + }, + ); + + test( + 'updateProfile updates metadata without touching secrets by default', + () async { + final credentials = _FakeCredentialsRepository(); + final container = _container(credentials); + addTearDown(container.dispose); + + final repository = container.read( + singboxProxyProfilesRepositoryProvider.notifier, + ); + final profile = _profile(id: 'profile-1', name: 'Original'); + await repository.updateProfile(profile); + await repository.updateProfile( + profile.copyWith(name: 'Updated', configJson: '{"type":"http"}'), + ); + + final profiles = await repository.fetchProfiles(); + + expect(profiles, hasLength(1)); + expect(profiles.single.id, 'profile-1'); + expect(profiles.single.name, 'Updated'); + expect(profiles.single.configJson, '{"type":"http"}'); + expect(credentials.writtenSecrets, isEmpty); + }, + ); + + test('updateProfile writes secrets only when requested', () async { + final credentials = _FakeCredentialsRepository(); + final container = _container(credentials); + addTearDown(container.dispose); + + final repository = container.read( + singboxProxyProfilesRepositoryProvider.notifier, + ); + final profile = _profile(id: 'profile-1', name: 'SOCKS'); + + await repository.updateProfile( + profile, + secretJson: '{"password":"secret"}', + ); + + expect(credentials.writtenSecrets, {'profile-1': '{"password":"secret"}'}); + }); + + test('deleteProfile removes metadata and deletes secrets', () async { + final credentials = _FakeCredentialsRepository(); + final container = _container(credentials); + addTearDown(container.dispose); + + final repository = container.read( + singboxProxyProfilesRepositoryProvider.notifier, + ); + await repository.updateProfile( + _profile(id: 'profile-1', name: 'SOCKS'), + secretJson: '{"password":"secret"}', + ); + + await repository.deleteProfile('profile-1'); + + expect(await repository.fetchProfiles(), isEmpty); + expect(credentials.deletedSecretIds, ['profile-1']); + }); +} + +ProviderContainer _container(_FakeCredentialsRepository credentials) { + final db = UserDatabase( + NativeDatabase.memory( + setup: (database) { + registerLexorankFunctions(database); + }, + ), + ); + addTearDown(db.close); + + return ProviderContainer( + overrides: [ + userDatabaseProvider.overrideWith((ref) => db), + singboxProxyCredentialsRepositoryProvider.overrideWith(() => credentials), + ], + ); +} + +ProxyProfile _profile({required String id, required String name}) { + final createdAt = DateTime(2026); + + return ProxyProfile( + id: id, + name: name, + type: SingboxProxyProfileType.socks, + configJson: '{"type":"socks"}', + createdAt: createdAt, + updatedAt: createdAt, + ); +} + +class _FakeCredentialsRepository extends SingboxProxyCredentialsRepository { + final writtenSecrets = {}; + final deletedSecretIds = []; + + @override + Future writeSecretJson(String profileId, String? secretJson) async { + writtenSecrets[profileId] = secretJson; + } + + @override + Future deleteSecretJson(String profileId) async { + deletedSecretIds.add(profileId); + } + + @override + void build() {} +} diff --git a/apps/weblibre/test/features/proxy/domain/repositories/singbox_proxy_runtime_test.dart b/apps/weblibre/test/features/proxy/domain/repositories/singbox_proxy_runtime_test.dart new file mode 100644 index 00000000..ea0bc9c7 --- /dev/null +++ b/apps/weblibre/test/features/proxy/domain/repositories/singbox_proxy_runtime_test.dart @@ -0,0 +1,451 @@ +import 'dart:async'; + +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:weblibre/features/proxy/data/models/singbox_proxy_profile.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_credentials.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/proxy/domain/services/singbox_proxy_endpoint_sync.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; +import 'package:weblibre/features/user/data/models/engine_settings.dart'; +import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test( + 'ensureProxyConnectionAvailable starts an assigned stopped profile', + () async { + final profile = _profile(id: 'profile-1', name: 'First'); + final client = _FakeSingboxProxyClient(_state(const [])); + final container = _container( + client: client, + profilesRepository: _FakeProfilesRepository([profile]), + ); + addTearDown(container.dispose); + + await container.read(singboxProxyRuntimeRepositoryProvider.future); + final repository = container.read( + singboxProxyRuntimeRepositoryProvider.notifier, + ); + + await repository.ensureProxyConnectionAvailable( + SingboxProxyConnectionId(profile.id), + ); + await repository.ensureProxyConnectionAvailable( + SingboxProxyConnectionId(profile.id), + ); + + expect(client.startCalls, hasLength(1)); + expect( + client.startCalls.single.map((runtimeProfile) => runtimeProfile.id), + [profile.proxyConnectionId], + ); + }, + ); + + test('startProfile preserves already-running profiles', () async { + final profile1 = _profile(id: 'profile-1', name: 'First'); + final profile2 = _profile(id: 'profile-2', name: 'Second'); + final client = _FakeSingboxProxyClient( + _state([_endpoint(profile1.proxyConnectionId, port: 12080)]), + ); + final container = _container( + client: client, + profilesRepository: _FakeProfilesRepository([profile1, profile2]), + ); + addTearDown(container.dispose); + + await container.read(singboxProxyRuntimeRepositoryProvider.future); + await container + .read(singboxProxyRuntimeRepositoryProvider.notifier) + .startProfile(profile2.id); + + expect(client.startCalls, hasLength(1)); + expect( + client.startCalls.single.map((profile) => profile.id), + unorderedEquals([profile1.proxyConnectionId, profile2.proxyConnectionId]), + ); + }); + + test( + 'concurrent startProfile calls preserve both requested profiles', + () async { + final profile1 = _profile(id: 'profile-1', name: 'First'); + final profile2 = _profile(id: 'profile-2', name: 'Second'); + final client = _FakeSingboxProxyClient(_state(const [])); + final container = _container( + client: client, + profilesRepository: _FakeProfilesRepository([profile1, profile2]), + ); + addTearDown(container.dispose); + + await container.read(singboxProxyRuntimeRepositoryProvider.future); + final repository = container.read( + singboxProxyRuntimeRepositoryProvider.notifier, + ); + + await Future.wait([ + repository.startProfile(profile1.id), + repository.startProfile(profile2.id), + ]); + + expect(client.startCalls, hasLength(2)); + expect( + client.startCalls.last.map((profile) => profile.id), + unorderedEquals([ + profile1.proxyConnectionId, + profile2.proxyConnectionId, + ]), + ); + expect( + container + .read(singboxProxyRuntimeRepositoryProvider) + .requireValue + .endpoints + .map((endpoint) => endpoint.profileId), + unorderedEquals([ + profile1.proxyConnectionId, + profile2.proxyConnectionId, + ]), + ); + }, + ); + + test('stopProfiles unregisters removed Gecko proxy endpoints', () async { + final profile1 = _profile(id: 'profile-1', name: 'First'); + final profile2 = _profile(id: 'profile-2', name: 'Second'); + final client = + _FakeSingboxProxyClient( + _state([ + _endpoint(profile1.proxyConnectionId, port: 12080), + _endpoint(profile2.proxyConnectionId, port: 12081), + ]), + ) + ..stateAfterStop = _state([ + _endpoint(profile1.proxyConnectionId, port: 12080), + ]); + final containerProxyRepository = _FakeContainerProxyRepository(); + final container = _container( + client: client, + profilesRepository: _FakeProfilesRepository([profile1, profile2]), + containerProxyRepository: containerProxyRepository, + ); + addTearDown(container.dispose); + + await container.read(singboxProxyRuntimeRepositoryProvider.future); + await container + .read(singboxProxyRuntimeRepositoryProvider.notifier) + .stopProfiles([profile2.id]); + await _drainSync(); + + expect(client.stopCalls, [profile2.proxyConnectionId]); + expect(containerProxyRepository.removedProxyIds, [ + profile2.proxyConnectionId, + ]); + }); + + test( + 'deleteProfile stops runtime and preserves persisted references', + () async { + final profile = _profile(id: 'profile-1', name: 'First'); + final client = _FakeSingboxProxyClient( + _state([_endpoint(profile.proxyConnectionId, port: 12080)]), + )..stateAfterStop = _state(const []); + final profilesRepository = _FakeProfilesRepository([profile]); + final containerRepository = _FakeContainerRepository(); + final routingSettingsRepository = _FakeProxyRoutingSettingsRepository( + ProxyRoutingSettings( + regularTabsMode: ProxyRegularTabRoutingMode.all, + regularTabsProxyConnectionId: profile.proxyConnection, + privateTabsProxyConnectionId: profile.proxyConnection, + ), + ); + final container = _container( + client: client, + profilesRepository: profilesRepository, + containerRepository: containerRepository, + routingSettingsRepository: routingSettingsRepository, + ); + addTearDown(container.dispose); + + await container.read(singboxProxyRuntimeRepositoryProvider.future); + await container + .read(singboxProxyRuntimeRepositoryProvider.notifier) + .deleteProfile(profile.id); + + expect(client.stopCalls, [profile.proxyConnectionId]); + expect(containerRepository.clearedProxyConnectionIds, isEmpty); + final routingSettings = routingSettingsRepository.peekSettings(); + + expect(routingSettings, isNotNull); + expect(routingSettings!.regularTabsMode, ProxyRegularTabRoutingMode.all); + expect( + routingSettings.regularTabsProxyConnectionId, + profile.proxyConnection, + ); + expect( + routingSettings.privateTabsProxyConnectionId, + profile.proxyConnection, + ); + expect(profilesRepository.deletedProfileIds, [profile.id]); + }, + ); + + test('runtime stream sync removes stale Gecko proxy registrations', () async { + final profile1 = _profile(id: 'profile-1', name: 'First'); + final profile2 = _profile(id: 'profile-2', name: 'Second'); + final client = _FakeSingboxProxyClient( + _state([ + _endpoint(profile1.proxyConnectionId, port: 12080), + _endpoint(profile2.proxyConnectionId, port: 12081), + ]), + ); + final containerProxyRepository = _FakeContainerProxyRepository(); + final container = _container( + client: client, + profilesRepository: _FakeProfilesRepository([profile1, profile2]), + containerProxyRepository: containerProxyRepository, + ); + addTearDown(container.dispose); + + await container.read(singboxProxyRuntimeRepositoryProvider.future); + client.emit(_state([_endpoint(profile2.proxyConnectionId, port: 12081)])); + await _drainSync(); + + expect(containerProxyRepository.removedProxyIds, [ + profile1.proxyConnectionId, + ]); + }); +} + +ProviderContainer _container({ + required _FakeSingboxProxyClient client, + required _FakeProfilesRepository profilesRepository, + _FakeContainerProxyRepository? containerProxyRepository, + _FakeContainerRepository? containerRepository, + _FakeProxyRoutingSettingsRepository? routingSettingsRepository, +}) { + final container = ProviderContainer( + overrides: [ + singboxProxyClientProvider.overrideWithValue(client), + singboxProxyProfilesRepositoryProvider.overrideWith( + () => profilesRepository, + ), + singboxProxyCredentialsRepositoryProvider.overrideWith( + _FakeCredentialsRepository.new, + ), + engineSettingsRepositoryProvider.overrideWith( + _FakeEngineSettingsRepository.new, + ), + containerProxyRepositoryProvider.overrideWith( + () => containerProxyRepository ?? _FakeContainerProxyRepository(), + ), + containerRepositoryProvider.overrideWith( + () => containerRepository ?? _FakeContainerRepository(), + ), + proxyRoutingSettingsRepositoryProvider.overrideWith( + () => + routingSettingsRepository ?? _FakeProxyRoutingSettingsRepository(), + ), + ], + ); + + // Activate the endpoint sync notifier so runtime state changes propagate + // into the fake container proxy repository. Production wires this in + // main.dart; tests have to ask for it explicitly. + container.read(singboxProxyEndpointSyncProvider); + + return container; +} + +/// Drains all currently-pending microtasks/futures so the endpoint-sync +/// `ref.listen` callback (chained off the runtime state stream listener) +/// has time to apply its registrations against the fake Gecko repo. +Future _drainSync() => pumpEventQueue(); + +ProxyProfile _profile({required String id, required String name}) { + final createdAt = DateTime(2026); + + return ProxyProfile( + id: id, + name: name, + type: SingboxProxyProfileType.customOutbound, + configJson: '{"type":"socks"}', + createdAt: createdAt, + updatedAt: createdAt, + ); +} + +SingboxProxyRuntimeState _state(List endpoints) { + return SingboxProxyRuntimeState( + status: endpoints.isEmpty + ? SingboxProxyRuntimeStatus.stopped + : SingboxProxyRuntimeStatus.running, + endpoints: endpoints, + ); +} + +SingboxProxyRuntimeEndpoint _endpoint(String profileId, {required int port}) { + return SingboxProxyRuntimeEndpoint( + profileId: profileId, + host: '127.0.0.1', + port: port, + username: 'user-$port', + password: 'pass-$port', + ); +} + +class _FakeSingboxProxyClient implements SingboxProxyClient { + final _stateController = + StreamController.broadcast(); + final startCalls = >[]; + final stopCalls = []; + SingboxProxyRuntimeState _currentState; + SingboxProxyRuntimeState? stateAfterStop; + + _FakeSingboxProxyClient(this._currentState); + + @override + Stream get stateStream => _stateController.stream; + + @override + Stream get logStream => const Stream.empty(); + + void emit(SingboxProxyRuntimeState state) { + _currentState = state; + _stateController.add(state); + } + + @override + Future getState() async => _currentState; + + @override + Future start( + List profiles, { + SingboxProxyRuntimeOptions? options, + }) async { + startCalls.add(profiles); + return _currentState = _state( + profiles.indexed + .map((item) => _endpoint(item.$2.id, port: 12080 + item.$1)) + .toList(), + ); + } + + @override + Future stop(List profileIds) async { + stopCalls.addAll(profileIds); + _currentState = stateAfterStop ?? _currentState; + } + + @override + Future stopAll() async { + _currentState = _state(const []); + } + + @override + Future validateProfile(SingboxProxyProfile profile) async => null; + + @override + Future buildConfig( + List profiles, { + SingboxProxyRuntimeOptions? options, + }) async { + return SingboxProxyConfigResult(configJson: '{}', endpoints: const []); + } + + @override + Future dispose() => _stateController.close(); +} + +class _FakeProfilesRepository extends SingboxProxyProfilesRepository { + final List profiles; + final deletedProfileIds = []; + + _FakeProfilesRepository(this.profiles); + + @override + Future> fetchProfiles() async => profiles; + + @override + Future deleteProfile(String profileId) async { + deletedProfileIds.add(profileId); + } + + @override + Stream> build() => Stream.value(profiles); +} + +class _FakeCredentialsRepository extends SingboxProxyCredentialsRepository { + @override + Future readSecretJson(String profileId) async => null; + + @override + void build() {} +} + +class _FakeEngineSettingsRepository extends EngineSettingsRepository { + final settings = EngineSettings.withDefaults(); + + @override + Future fetchSettings() async => settings; + + @override + Stream build() => Stream.value(settings); +} + +class _FakeContainerProxyRepository extends ContainerProxyRepository { + final upsertedProxies = []; + final removedProxyIds = []; + + @override + Future upsertProxy(GeckoProxySettings proxy) async { + upsertedProxies.add(proxy); + } + + @override + Future removeProxy(String proxyId) async { + removedProxyIds.add(proxyId); + } + + @override + void build() {} +} + +class _FakeContainerRepository extends ContainerRepository { + final clearedProxyConnectionIds = []; + + @override + Future clearProxyConnectionAssignments( + ProxyConnectionId proxyConnectionId, + ) async { + clearedProxyConnectionIds.add(proxyConnectionId); + } + + @override + void build() {} +} + +class _FakeProxyRoutingSettingsRepository + extends ProxyRoutingSettingsRepository { + ProxyRoutingSettings? _settings; + + _FakeProxyRoutingSettingsRepository([this._settings]); + + ProxyRoutingSettings? peekSettings() => _settings; + + @override + Stream build() { + return Stream.value(_settings ?? ProxyRoutingSettings.withDefaults()); + } +} diff --git a/apps/weblibre/test/features/proxy/domain/services/dns_config_resolver_test.dart b/apps/weblibre/test/features/proxy/domain/services/dns_config_resolver_test.dart new file mode 100644 index 00000000..d922a09e --- /dev/null +++ b/apps/weblibre/test/features/proxy/domain/services/dns_config_resolver_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:weblibre/features/proxy/domain/services/dns_config_resolver.dart'; +import 'package:weblibre/features/user/data/models/proxy_dns_override.dart'; + +void main() { + test('mirrors browser DoH through running profiles automatically', () { + final config = buildDnsConfig( + overridesByProfileId: const {'profile-1': null}, + runningProfileIds: const {'profile-1'}, + browserDohUrl: 'https://dns.example/dns-query', + ); + + expect(config, isNotNull); + expect(config!.finalServerTag, 'browser-doh'); + expect(config.servers, hasLength(2)); + expect(config.servers.first.detourTag, isNull); + expect(config.servers.last.tag, 'browser-doh-profile-1'); + expect(config.servers.last.detourTag, 'out-singbox_profile-1'); + expect(config.servers.last.matchInbounds, ['in-singbox_profile-1']); + }); + + test('profile override replaces automatic browser DoH mirror', () { + final config = buildDnsConfig( + overridesByProfileId: { + 'profile-1': ProxyDnsOverride(remoteServerAddress: '10.0.0.53'), + }, + runningProfileIds: const {'profile-1'}, + browserDohUrl: 'https://dns.example/dns-query', + ); + + expect(config, isNotNull); + expect(config!.servers.map((s) => s.tag), [ + 'browser-doh', + 'override-profile-1', + ]); + expect(config.servers.last.address, '10.0.0.53'); + expect(config.servers.last.detourTag, 'out-singbox_profile-1'); + expect(config.servers.last.matchInbounds, ['in-singbox_profile-1']); + }); + + test('returns null when there is no browser DoH or override', () { + final config = buildDnsConfig( + overridesByProfileId: const {'profile-1': null}, + runningProfileIds: const {'profile-1'}, + browserDohUrl: null, + ); + + expect(config, isNull); + }); + + test('auto domain strategy emits sing-box as-is strategy', () { + expect(ProxyDnsDomainStrategy.auto.singboxValue, isEmpty); + }); +} diff --git a/apps/weblibre/test/features/proxy/domain/services/singbox_tag_format_test.dart b/apps/weblibre/test/features/proxy/domain/services/singbox_tag_format_test.dart new file mode 100644 index 00000000..2c6bf56a --- /dev/null +++ b/apps/weblibre/test/features/proxy/domain/services/singbox_tag_format_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:weblibre/features/proxy/domain/services/dns_config_resolver.dart'; + +/// The Dart and Kotlin sides of the sing-box config builder must produce the +/// same `out-`/`in-` tags. These cases mirror `SingboxTagFormatTest.kt` — +/// both must update together if this format ever changes. +void main() { + group('singbox tag format', () { + test('outboundTag is prefixed and sanitized', () { + expect(singboxOutboundTag('singbox:foo-bar'), 'out-singbox_foo-bar'); + }); + + test('inboundTag is prefixed and sanitized', () { + expect(singboxInboundTag('singbox:foo-bar'), 'in-singbox_foo-bar'); + }); + + test('sanitizeTag preserves alphanumeric, dots, dashes, underscores', () { + expect(singboxSanitizeTag('Abc_123.x-Y'), 'Abc_123.x-Y'); + }); + + test('sanitizeTag replaces spaces, slashes, colons', () { + expect(singboxSanitizeTag('a:b c/d\\e'), 'a_b_c_d_e'); + }); + }); +} diff --git a/apps/weblibre/test/features/proxy/presentation/controllers/ensure_proxy_started_test.dart b/apps/weblibre/test/features/proxy/presentation/controllers/ensure_proxy_started_test.dart new file mode 100644 index 00000000..d31627a5 --- /dev/null +++ b/apps/weblibre/test/features/proxy/presentation/controllers/ensure_proxy_started_test.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/ensure_proxy_started.dart'; + +void main() { + testWidgets( + 'prompts and retries sing-box start when runtime provider is in error', + (tester) async { + final runtimeRepository = _ErrorRuntimeRepository(); + final container = ContainerData( + id: 'container-1', + color: Colors.blue, + orderKey: 'a', + metadata: ContainerMetadata.withDefaults( + proxyConnectionId: const SingboxProxyConnectionId('profile-1'), + ), + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + singboxProxyRuntimeRepositoryProvider.overrideWith( + () => runtimeRepository, + ), + proxyConnectionOptionsProvider.overrideWith( + (ref) => [ + ProxyConnectionOption( + id: const SingboxProxyConnectionId('profile-1'), + title: 'Mullvad', + subtitle: 'SOCKS', + ), + ], + ), + ], + child: MaterialApp(home: _EnsureProxyHarness(container: container)), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Open container')); + await tester.pumpAndSettle(); + + expect(find.text('Start Proxy Connection?'), findsOneWidget); + expect( + find.textContaining('This container uses Mullvad'), + findsOneWidget, + ); + + await tester.tap(find.text('Start')); + await tester.pumpAndSettle(); + + expect(runtimeRepository.startedProfileIds, ['profile-1']); + expect(find.text('result:true'), findsOneWidget); + }, + ); +} + +class _EnsureProxyHarness extends ConsumerStatefulWidget { + final ContainerData container; + + const _EnsureProxyHarness({required this.container}); + + @override + ConsumerState<_EnsureProxyHarness> createState() => + _EnsureProxyHarnessState(); +} + +class _EnsureProxyHarnessState extends ConsumerState<_EnsureProxyHarness> { + bool? _result; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Text('result:${_result ?? 'pending'}'), + TextButton( + onPressed: () async { + final result = await ensureProxyStartedForContainer( + context, + ref, + widget.container, + ); + if (!mounted) return; + setState(() { + _result = result; + }); + }, + child: const Text('Open container'), + ), + ], + ), + ); + } +} + +class _ErrorRuntimeRepository extends SingboxProxyRuntimeRepository { + final startedProfileIds = []; + + @override + Future build() { + return Future.error(StateError('runtime failed')); + } + + @override + Future startProfile( + String profileId, { + SingboxProxyRuntimeOptions? options, + }) async { + startedProfileIds.add(profileId); + return SingboxProxyRuntimeState( + status: SingboxProxyRuntimeStatus.running, + endpoints: [ + SingboxProxyRuntimeEndpoint( + profileId: const SingboxProxyConnectionId('profile-1').encode(), + host: '127.0.0.1', + port: 1080, + username: '', + password: '', + ), + ], + ); + } +} diff --git a/apps/weblibre/test/features/proxy/presentation/controllers/proxy_profile_draft_controller_test.dart b/apps/weblibre/test/features/proxy/presentation/controllers/proxy_profile_draft_controller_test.dart new file mode 100644 index 00000000..7f6ce5dd --- /dev/null +++ b/apps/weblibre/test/features/proxy/presentation/controllers/proxy_profile_draft_controller_test.dart @@ -0,0 +1,207 @@ +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_profile_seed.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_credentials.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/proxy/presentation/controllers/proxy_profile_draft_controller.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; + +void main() { + test('save validates and creates a seeded structured profile', () async { + final runtimeRepository = _FakeRuntimeRepository(); + final profilesRepository = _FakeProfilesRepository(); + final container = _container( + runtimeRepository: runtimeRepository, + profilesRepository: profilesRepository, + ); + addTearDown(container.dispose); + + final provider = proxyProfileDraftProvider( + seed: ProxyProfileSeed( + type: SingboxProxyProfileType.socks, + name: 'Imported SOCKS', + values: { + 'server': 'proxy.example', + 'server_port': '1080', + 'username': 'alice', + 'password': 'secret', + }, + ), + ); + + final outcome = await container.read(provider.notifier).save(); + + expect(outcome, isA()); + expect(runtimeRepository.validatedProfiles, hasLength(1)); + expect(profilesRepository.createdProfiles, hasLength(1)); + expect(profilesRepository.createdProfiles.single.name, 'Imported SOCKS'); + expect( + profilesRepository.createdProfiles.single.configJson, + contains('"server": "proxy.example"'), + ); + expect( + profilesRepository.createdSecrets.single, + contains('"password": "secret"'), + ); + }); + + test('editing writes null secret to clear secure storage', () async { + final credentialsRepository = _FakeCredentialsRepository( + secrets: {'profile-1': '{"password":"old-secret"}'}, + ); + final profilesRepository = _FakeProfilesRepository( + existingProfile: _profile( + id: 'profile-1', + name: 'SOCKS', + type: SingboxProxyProfileType.socks, + configJson: + '{"type":"socks","server":"proxy.example","server_port":1080}', + ), + ); + final container = _container( + profilesRepository: profilesRepository, + credentialsRepository: credentialsRepository, + ); + addTearDown(container.dispose); + + final provider = proxyProfileDraftProvider(profileId: 'profile-1'); + final subscription = container.listen( + provider, + (_, _) {}, + fireImmediately: true, + ); + addTearDown(subscription.close); + await _drainAsyncLoad(); + + container.read(provider.notifier).setFieldValue('password', ''); + final outcome = await container.read(provider.notifier).save(); + + expect(outcome, isA()); + expect(profilesRepository.updatedProfiles, hasLength(1)); + expect(credentialsRepository.writtenSecrets, {'profile-1': null}); + }); +} + +ProviderContainer _container({ + _FakeRuntimeRepository? runtimeRepository, + _FakeProfilesRepository? profilesRepository, + _FakeCredentialsRepository? credentialsRepository, +}) { + return ProviderContainer( + overrides: [ + singboxProxyRuntimeRepositoryProvider.overrideWith( + () => runtimeRepository ?? _FakeRuntimeRepository(), + ), + singboxProxyProfilesRepositoryProvider.overrideWith( + () => profilesRepository ?? _FakeProfilesRepository(), + ), + singboxProxyCredentialsRepositoryProvider.overrideWith( + () => credentialsRepository ?? _FakeCredentialsRepository(), + ), + ], + ); +} + +Future _drainAsyncLoad() => pumpEventQueue(); + +ProxyProfile _profile({ + required String id, + required String name, + required SingboxProxyProfileType type, + required String configJson, +}) { + final createdAt = DateTime(2026); + return ProxyProfile( + id: id, + name: name, + type: type, + configJson: configJson, + createdAt: createdAt, + updatedAt: createdAt, + ); +} + +class _FakeRuntimeRepository extends SingboxProxyRuntimeRepository { + final validatedProfiles = []; + final validatedSecrets = []; + + @override + Future validateProfileDraft( + ProxyProfile profile, { + String? secretJson, + }) async { + validatedProfiles.add(profile); + validatedSecrets.add(secretJson); + return null; + } + + @override + Future build() async { + return SingboxProxyRuntimeState( + status: SingboxProxyRuntimeStatus.stopped, + endpoints: const [], + ); + } +} + +class _FakeProfilesRepository extends SingboxProxyProfilesRepository { + final ProxyProfile? existingProfile; + final createdProfiles = []; + final createdSecrets = []; + final updatedProfiles = []; + + _FakeProfilesRepository({this.existingProfile}); + + @override + Future findProfile(String id) async => existingProfile; + + @override + Future createProfile({ + required String name, + required SingboxProxyProfileType type, + required String configJson, + String? secretJson, + String? dnsOverrideJson, + }) async { + final profile = _profile( + id: 'created-profile', + name: name, + type: type, + configJson: configJson, + ); + createdProfiles.add(profile); + createdSecrets.add(secretJson); + return profile; + } + + @override + Future updateProfile(ProxyProfile profile, {String? secretJson}) async { + updatedProfiles.add(profile); + } + + @override + Stream> build() { + return Stream.value([if (existingProfile != null) existingProfile!]); + } +} + +class _FakeCredentialsRepository extends SingboxProxyCredentialsRepository { + final Map secrets; + final writtenSecrets = {}; + + _FakeCredentialsRepository({this.secrets = const {}}); + + @override + Future readSecretJson(String profileId) async => secrets[profileId]; + + @override + Future writeSecretJson(String profileId, String? secretJson) async { + writtenSecrets[profileId] = secretJson; + } + + @override + void build() {} +} diff --git a/apps/weblibre/test/features/proxy/presentation/screens/singbox_proxy_profiles_test.dart b/apps/weblibre/test/features/proxy/presentation/screens/singbox_proxy_profiles_test.dart new file mode 100644 index 00000000..f80dce66 --- /dev/null +++ b/apps/weblibre/test/features/proxy/presentation/screens/singbox_proxy_profiles_test.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/proxy/data/models/proxy_profile_seed.dart'; +import 'package:weblibre/features/proxy/data/proxy_connection.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_credentials.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart'; +import 'package:weblibre/features/proxy/presentation/screens/singbox_proxy_profile_editor.dart'; +import 'package:weblibre/features/proxy/presentation/screens/singbox_proxy_profiles.dart'; +import 'package:weblibre/features/proxy/presentation/widgets/profile_list/profile_tile.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart' + show ProxyProfile; + +void main() { + testWidgets('deleting a running profile stops through runtime repository', ( + tester, + ) async { + final profile = _profile(id: 'profile-1', name: 'Mullvad'); + final profilesRepository = _FakeProfilesRepository([profile]); + final runtimeRepository = _FakeRuntimeRepository( + SingboxProxyRuntimeState( + status: SingboxProxyRuntimeStatus.running, + endpoints: [ + SingboxProxyRuntimeEndpoint( + profileId: SingboxProxyConnectionId(profile.id).encode(), + host: '127.0.0.1', + port: 12080, + username: 'user', + password: 'pass', + ), + ], + ), + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + singboxProxyProfilesRepositoryProvider.overrideWith( + () => profilesRepository, + ), + singboxProxyRuntimeRepositoryProvider.overrideWith( + () => runtimeRepository, + ), + ], + child: const MaterialApp(home: SingboxProxyProfilesScreen()), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Mullvad'), findsOneWidget); + + await tester.tap( + find.descendant( + of: find.byType(ProfileTile), + matching: find.byIcon(Icons.more_vert), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + expect(find.text('Stop and Delete'), findsOneWidget); + + await tester.tap(find.text('Stop and Delete')); + await tester.pumpAndSettle(); + + expect(runtimeRepository.deletedProfileIds, ['profile-1']); + }); + + testWidgets('proxy URI import populates structured editor fields', ( + tester, + ) async { + await _pumpEditor( + tester, + profilesRepository: _FakeProfilesRepository([]), + seed: ProxyProfileSeed( + type: SingboxProxyProfileType.vless, + name: 'Imported VLESS', + values: { + 'server': 'vless.example.com', + 'server_port': '443', + 'uuid': '00000000-0000-0000-0000-000000000000', + 'flow': 'xtls-rprx-vision', + 'tls.server_name': 'front.example.com', + }, + ), + ); + + expect(find.text('Imported VLESS'), findsOneWidget); + expect(find.text('VLESS'), findsOneWidget); + expect(find.text('Connection'), findsWidgets); + expect(find.text('Credentials'), findsOneWidget); + expect(find.text('TLS'), findsOneWidget); + expect(_fieldText(tester, 'Server Address *'), 'vless.example.com'); + expect(_fieldText(tester, 'Server Port *'), '443'); + expect( + _fieldText(tester, 'UUID *'), + '00000000-0000-0000-0000-000000000000', + ); + }); + + testWidgets('WireGuard config import populates WireGuard form fields', ( + tester, + ) async { + await _pumpEditor( + tester, + profilesRepository: _FakeProfilesRepository([]), + seed: ProxyProfileSeed( + type: SingboxProxyProfileType.wireguard, + values: { + 'server': 'wg.example.com', + 'server_port': '51820', + 'local_address': '10.0.0.2/32', + 'private_key': 'private-key', + 'peer_public_key': 'peer-public-key', + }, + ), + ); + + expect(_fieldText(tester, 'Server Address *'), 'wg.example.com'); + expect(_fieldText(tester, 'Server Port *'), '51820'); + expect(_fieldText(tester, 'Local Address *'), '10.0.0.2/32'); + expect(_fieldText(tester, 'Private Key *'), 'private-key'); + }); +} + +Future _pumpEditor( + WidgetTester tester, { + required _FakeProfilesRepository profilesRepository, + String? profileId, + ProxyProfileSeed? seed, +}) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + singboxProxyProfilesRepositoryProvider.overrideWith( + () => profilesRepository, + ), + singboxProxyCredentialsRepositoryProvider.overrideWith( + _FakeCredentialsRepository.new, + ), + singboxProxyRuntimeRepositoryProvider.overrideWith( + () => _FakeRuntimeRepository( + SingboxProxyRuntimeState( + status: SingboxProxyRuntimeStatus.stopped, + endpoints: const [], + ), + ), + ), + ], + child: MaterialApp( + home: SingboxProxyProfileEditorScreen(profileId: profileId, seed: seed), + ), + ), + ); + await tester.pumpAndSettle(); +} + +String _fieldText(WidgetTester tester, String labelText) { + final textField = tester.widget( + find.ancestor(of: find.text(labelText), matching: find.byType(TextField)), + ); + return textField.controller!.text; +} + +ProxyProfile _profile({ + required String id, + required String name, + SingboxProxyProfileType type = SingboxProxyProfileType.customOutbound, + String configJson = '{"type":"socks"}', +}) { + final createdAt = DateTime(2026); + + return ProxyProfile( + id: id, + name: name, + type: type, + configJson: configJson, + createdAt: createdAt, + updatedAt: createdAt, + ); +} + +class _FakeProfilesRepository extends SingboxProxyProfilesRepository { + final List profiles; + + _FakeProfilesRepository(this.profiles); + + @override + Stream> build() => Stream.value(profiles); +} + +class _FakeCredentialsRepository extends SingboxProxyCredentialsRepository { + @override + Future readSecretJson(String profileId) async => null; + + @override + void build() {} +} + +class _FakeRuntimeRepository extends SingboxProxyRuntimeRepository { + final SingboxProxyRuntimeState initialState; + final deletedProfileIds = []; + + _FakeRuntimeRepository(this.initialState); + + @override + Future build() async => initialState; + + @override + Future deleteProfile(String profileId) async { + deletedProfileIds.add(profileId); + } +} diff --git a/apps/weblibre/test/features/web_search/domain/controllers/search_controller_test.dart b/apps/weblibre/test/features/web_search/domain/controllers/search_controller_test.dart index e9501b1e..222dff4a 100644 --- a/apps/weblibre/test/features/web_search/domain/controllers/search_controller_test.dart +++ b/apps/weblibre/test/features/web_search/domain/controllers/search_controller_test.dart @@ -24,7 +24,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:riverpod/experimental/persist.dart'; import 'package:riverpod/riverpod.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:weblibre/features/search_credits/domain/controllers/search_token_issuance_controller.dart'; import 'package:weblibre/features/user/data/providers.dart'; import 'package:weblibre/features/web_search/domain/controllers/search_controller.dart'; diff --git a/apps/weblibre/test/features/web_search/domain/services/capture_server_test.dart b/apps/weblibre/test/features/web_search/domain/services/capture_server_test.dart index 81346754..817005c6 100644 --- a/apps/weblibre/test/features/web_search/domain/services/capture_server_test.dart +++ b/apps/weblibre/test/features/web_search/domain/services/capture_server_test.dart @@ -125,23 +125,25 @@ void main() { expect(decoded['url'], contains('t=')); }); - test('err=1 starts the loader in the error pane with a retry button', - () async { - final url = await server.loaderUrl( - tabId: 'tab-9', - captureId: 'failed9', - error: true, - ); - final res = await _get(url); - expect(res.statusCode, 200); - // Retry is now a JS-driven button, not an HTML form — assert the - // shell starts in error mode (error pane visible, pending hidden) - // and that the button + JS retry endpoint string are present. - expect(res.body, contains('id="retry"')); - expect(res.body, contains('id="error" class="error"')); - expect(res.body, contains('id="pending" class="hidden"')); - expect(res.body, contains('/loader/retry?tab=')); - }); + test( + 'err=1 starts the loader in the error pane with a retry button', + () async { + final url = await server.loaderUrl( + tabId: 'tab-9', + captureId: 'failed9', + error: true, + ); + final res = await _get(url); + expect(res.statusCode, 200); + // Retry is now a JS-driven button, not an HTML form — assert the + // shell starts in error mode (error pane visible, pending hidden) + // and that the button + JS retry endpoint string are present. + expect(res.body, contains('id="retry"')); + expect(res.body, contains('id="error" class="error"')); + expect(res.body, contains('id="pending" class="hidden"')); + expect(res.body, contains('/loader/retry?tab=')); + }, + ); test('loader with invalid capture id returns 404', () async { final port = await server.ensureStarted(); @@ -174,9 +176,7 @@ void main() { // Start the long-poll while nothing has been published yet — it // should block on the per-id completer, not busy-poll the filesystem. final pollFuture = _get( - Uri.parse( - 'http://127.0.0.1:$port/loader/wait?tab=t&capture=signaled', - ), + Uri.parse('http://127.0.0.1:$port/loader/wait?tab=t&capture=signaled'), ); // Race-free: publish only after the request hit the handler. A short // microtask flush gives the handler time to register its waiter. @@ -192,23 +192,25 @@ void main() { }); group('retry route', () { - test('emits a RetryRequest on retryRequests stream and returns 204', - () async { - final port = await server.ensureStarted(); - final retryFuture = server.retryRequests.first; + test( + 'emits a RetryRequest on retryRequests stream and returns 204', + () async { + final port = await server.ensureStarted(); + final retryFuture = server.retryRequests.first; - final res = await _post( - Uri.parse( - 'http://127.0.0.1:$port/loader/retry?tab=tab-42&capture=cap42', - ), - ); - // 204 No Content — the JS loader re-polls; a body would be wasted. - expect(res.statusCode, 204); + final res = await _post( + Uri.parse( + 'http://127.0.0.1:$port/loader/retry?tab=tab-42&capture=cap42', + ), + ); + // 204 No Content — the JS loader re-polls; a body would be wasted. + expect(res.statusCode, 204); - final req = await retryFuture.timeout(const Duration(seconds: 2)); - expect(req.tabId, 'tab-42'); - expect(req.captureId, 'cap42'); - }); + final req = await retryFuture.timeout(const Duration(seconds: 2)); + expect(req.tabId, 'tab-42'); + expect(req.captureId, 'cap42'); + }, + ); test('retry rejects GET with 405', () async { final port = await server.ensureStarted(); diff --git a/apps/weblibre/test/features/web_search/presentation/screens/page_preview_test.dart b/apps/weblibre/test/features/web_search/presentation/screens/page_preview_test.dart index 4d5bb6f7..b74fc149 100644 --- a/apps/weblibre/test/features/web_search/presentation/screens/page_preview_test.dart +++ b/apps/weblibre/test/features/web_search/presentation/screens/page_preview_test.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart'; import 'package:weblibre/features/user/domain/providers.dart'; @@ -79,14 +79,14 @@ void main() { ), ), ); - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 100)); expect(find.text('Heading'), findsOneWidget); expect(find.text('Body copy'), findsOneWidget); expect(find.text('Fabian'), findsOneWidget); await tester.tap(find.byTooltip('Open in browser')); - await tester.pumpAndSettle(); + await tester.pump(); expect(opener.openedUris, [Uri.parse(url)]); }); diff --git a/apps/weblibre/test/features/web_search/presentation/widgets/search_result_card_test.dart b/apps/weblibre/test/features/web_search/presentation/widgets/search_result_card_test.dart index 6e4a2336..7692c4ef 100644 --- a/apps/weblibre/test/features/web_search/presentation/widgets/search_result_card_test.dart +++ b/apps/weblibre/test/features/web_search/presentation/widgets/search_result_card_test.dart @@ -23,7 +23,9 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:search_backend/search_backend.dart'; +import 'package:search_protocol/search_protocol.dart'; +import 'package:weblibre/features/search_credits/data/models/web_search_settings.dart'; +import 'package:weblibre/features/search_credits/domain/repositories/web_search_settings.dart'; import 'package:weblibre/features/user/domain/providers.dart'; import 'package:weblibre/features/web_search/domain/controllers/search_controller.dart'; import 'package:weblibre/features/web_search/presentation/widgets/search_result_card.dart'; @@ -39,12 +41,15 @@ void main() { ProviderScope( overrides: [ ...webSearchTestOverrides(), - metaSearchControllerProvider.overrideWithValue( - MetaSearchState(status: WebSearchStatus.ready), - ), watchCachedIconBytesProvider.overrideWith((ref, origin) { return Stream.value(null); }), + metaSearchControllerProvider.overrideWithValue( + const MetaSearchState(status: WebSearchStatus.ready), + ), + webSearchSettingsControllerProvider.overrideWithValue( + WebSearchSettings.withDefaults(), + ), ], child: MaterialApp( home: Scaffold( @@ -67,7 +72,7 @@ void main() { ); await tester.tap(find.text('LensAI result')); - await tester.pumpAndSettle(); + await tester.pump(); expect(openedUris, [Uri.parse(url)]); }); @@ -84,15 +89,18 @@ void main() { ProviderScope( overrides: [ ...webSearchTestOverrides(), + watchCachedIconBytesProvider.overrideWith((ref, origin) { + return Stream.value(null); + }), metaSearchControllerProvider.overrideWithValue( MetaSearchState( status: WebSearchStatus.ready, imagesByUrl: {thumbnailUrl: Uint8List.fromList(pngBytes)}, ), ), - watchCachedIconBytesProvider.overrideWith((ref, origin) { - return Stream.value(null); - }), + webSearchSettingsControllerProvider.overrideWithValue( + WebSearchSettings.withDefaults(), + ), ], child: MaterialApp( home: Scaffold( @@ -114,7 +122,7 @@ void main() { ), ); - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 100)); expect(find.byType(Image), findsOneWidget); }, diff --git a/native/go_mobile_runtime/README.md b/native/go_mobile_runtime/README.md new file mode 100644 index 00000000..123e5097 --- /dev/null +++ b/native/go_mobile_runtime/README.md @@ -0,0 +1,38 @@ +# Go Mobile Runtime + +This directory builds the single Android `gomobile` AAR used by WebLibre. + +`gomobile` does not support multiple Go mobile AARs in one Android app because +the generated Go runtime/JNI symbols collide. WebLibre therefore binds sing-box +`libbox` and IPtProxy in one `gomobile bind` invocation. + +Pinned inputs are defined in `pins.env`: + +- sing-box `v1.13.12` +- IPtProxy `5.4.2` +- IPtProxy's `dnstt` submodule commit `f1b9b97a269f83bad41d2ceef291b4d2c161cd11` +- `github.com/sagernet/gomobile@v0.1.12` + +Build locally from the repository root: + +```sh +native/go_mobile_runtime/scripts/build-android.sh +``` + +The default source locations are sibling checkouts of this repository: + +- `../sing-box` +- `../IPtProxy` + +IPtProxy requires its `dnstt` submodule. Initialize it in the IPtProxy checkout +or pass `--dnstt-source /path/to/dnstt`. + +Override them when needed: + +```sh +native/go_mobile_runtime/scripts/build-android.sh \ + --sing-box-source /path/to/sing-box \ + --iptproxy-source /path/to/IPtProxy +``` + +The output AAR is written to `native/go_mobile_runtime/build/weblibre-go.aar`. diff --git a/native/go_mobile_runtime/pins.env b/native/go_mobile_runtime/pins.env new file mode 100644 index 00000000..d61e20dc --- /dev/null +++ b/native/go_mobile_runtime/pins.env @@ -0,0 +1,12 @@ +# Pinned source revisions for the combined gomobile Android runtime. + +SING_BOX_TAG=v1.13.12 +SING_BOX_COMMIT=1086ab2563320e0da0c23b3a491d8dfa0939dff4 + +IPTPROXY_TAG=5.4.2 +IPTPROXY_COMMIT=3b99b6b1f4d5b51aea97d7213bc36e74ec77c84d + +DNSTT_COMMIT=f1b9b97a269f83bad41d2ceef291b4d2c161cd11 + +GOMOBILE_MODULE=github.com/sagernet/gomobile +GOMOBILE_VERSION=v0.1.12 diff --git a/native/go_mobile_runtime/scripts/build-android.sh b/native/go_mobile_runtime/scripts/build-android.sh new file mode 100755 index 00000000..d6d0ae39 --- /dev/null +++ b/native/go_mobile_runtime/scripts/build-android.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +RUNTIME_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" +REPO_ROOT="$(cd "$RUNTIME_DIR/../.." && pwd -P)" + +# shellcheck source=../pins.env +source "$RUNTIME_DIR/pins.env" + +DEFAULT_SING_BOX_SOURCE="$(cd "$REPO_ROOT/.." && pwd -P)/sing-box" +DEFAULT_IPTPROXY_SOURCE="$(cd "$REPO_ROOT/.." && pwd -P)/IPtProxy" + +SING_BOX_SOURCE="${SING_BOX_SOURCE:-$DEFAULT_SING_BOX_SOURCE}" +IPTPROXY_SOURCE="${IPTPROXY_SOURCE:-$DEFAULT_IPTPROXY_SOURCE}" +DNSTT_SOURCE="${DNSTT_SOURCE:-$IPTPROXY_SOURCE/dnstt}" +OUTPUT_AAR="${OUTPUT_AAR:-$RUNTIME_DIR/build/weblibre-go.aar}" +TARGET="${TARGET:-android}" +ANDROID_API="${ANDROID_API:-24}" +INSTALL_GOMOBILE=1 +ALLOW_DIRTY=0 +DEBUG=0 + +DEFAULT_TAGS="with_conntrack,with_gvisor,with_quic,with_wireguard,with_utls,with_naive_outbound,with_clash_api,badlinkname,tfogo_checklinkname0,netcgo" +GO_MOBILE_TAGS="${GO_MOBILE_TAGS:-$DEFAULT_TAGS}" + +usage() { + cat <<'EOF' +Usage: build-android.sh [options] + +Build the combined WebLibre gomobile Android AAR containing sing-box libbox and IPtProxy. + +Options: + --sing-box-source PATH sing-box checkout (default: ../sing-box) + --iptproxy-source PATH IPtProxy checkout (default: ../IPtProxy) + --dnstt-source PATH dnstt checkout (default: /dnstt) + --output PATH destination AAR (default: native/go_mobile_runtime/build/weblibre-go.aar) + --target TARGET gomobile target, e.g. android/arm64 (default: android) + --android-api API Android API level for gomobile bind (default: 24) + --tags TAGS comma-separated Go build tags + --debug keep debug symbols + --allow-dirty allow dirty sing-box/IPtProxy source trees + --skip-install-gomobile require gomobile/gobind in GOPATH/bin + -h, --help show this help + +Environment: + SING_BOX_SOURCE, IPTPROXY_SOURCE, DNSTT_SOURCE, OUTPUT_AAR, TARGET, + ANDROID_API, GO_MOBILE_TAGS, JAVA_HOME, ANDROID_HOME, ANDROID_NDK_HOME +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --sing-box-source) + SING_BOX_SOURCE="$2" + shift 2 + ;; + --iptproxy-source) + IPTPROXY_SOURCE="$2" + shift 2 + ;; + --dnstt-source) + DNSTT_SOURCE="$2" + shift 2 + ;; + --output) + OUTPUT_AAR="$2" + shift 2 + ;; + --target) + TARGET="$2" + shift 2 + ;; + --android-api) + ANDROID_API="$2" + shift 2 + ;; + --tags) + GO_MOBILE_TAGS="$2" + shift 2 + ;; + --debug) + DEBUG=1 + shift + ;; + --allow-dirty) + ALLOW_DIRTY=1 + shift + ;; + --skip-install-gomobile) + INSTALL_GOMOBILE=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +require_dir() { + local path="$1" + local description="$2" + if [[ ! -d "$path" ]]; then + echo "$description not found at: $path" >&2 + exit 1 + fi +} + +require_file() { + local path="$1" + local description="$2" + if [[ ! -f "$path" ]]; then + echo "$description not found at: $path" >&2 + exit 1 + fi +} + +verify_git_pin() { + local repo="$1" + local name="$2" + local tag="$3" + local expected_commit="$4" + + local tag_commit + tag_commit="$(git -C "$repo" rev-parse --verify "refs/tags/$tag^{commit}")" + if [[ "$tag_commit" != "$expected_commit" ]]; then + echo "$name tag $tag resolves to $tag_commit, expected $expected_commit" >&2 + exit 1 + fi + + local head_commit + head_commit="$(git -C "$repo" rev-parse HEAD)" + if [[ "$head_commit" != "$expected_commit" ]]; then + echo "$name checkout is at $head_commit, expected $tag ($expected_commit)." >&2 + echo "Check out the pinned tag first: git -C '$repo' checkout '$tag'" >&2 + exit 1 + fi + + if [[ $ALLOW_DIRTY -eq 0 && -n "$(git -C "$repo" status --porcelain)" ]]; then + echo "$name checkout has uncommitted changes. Re-run with --allow-dirty to build anyway." >&2 + exit 1 + fi +} + +require_dir "$SING_BOX_SOURCE" "sing-box source" +require_file "$SING_BOX_SOURCE/go.mod" "sing-box go.mod" +require_dir "$SING_BOX_SOURCE/experimental/libbox" "sing-box libbox package" + +require_dir "$IPTPROXY_SOURCE" "IPtProxy source" +require_file "$IPTPROXY_SOURCE/IPtProxy.go/go.mod" "IPtProxy go.mod" +require_dir "$DNSTT_SOURCE" "dnstt source" +require_file "$DNSTT_SOURCE/go.mod" "dnstt go.mod" + +verify_git_pin "$SING_BOX_SOURCE" "sing-box" "$SING_BOX_TAG" "$SING_BOX_COMMIT" +verify_git_pin "$IPTPROXY_SOURCE" "IPtProxy" "$IPTPROXY_TAG" "$IPTPROXY_COMMIT" + +read -r _ _ iptproxy_dnstt_commit _ <<< "$(git -C "$IPTPROXY_SOURCE" ls-tree "$IPTPROXY_TAG" dnstt)" +if [[ "$iptproxy_dnstt_commit" != "$DNSTT_COMMIT" ]]; then + echo "IPtProxy tag $IPTPROXY_TAG points dnstt to $iptproxy_dnstt_commit, expected $DNSTT_COMMIT" >&2 + exit 1 +fi + +if [[ -d "$DNSTT_SOURCE/.git" ]]; then + dnstt_head="$(git -C "$DNSTT_SOURCE" rev-parse HEAD)" + if [[ "$dnstt_head" != "$DNSTT_COMMIT" ]]; then + echo "dnstt checkout is at $dnstt_head, expected $DNSTT_COMMIT." >&2 + exit 1 + fi +fi + +if [[ -z "${JAVA_HOME:-}" && -x /usr/lib/jvm/java-17-openjdk/bin/java ]]; then + export JAVA_HOME=/usr/lib/jvm/java-17-openjdk +fi + +JAVA_BIN="${JAVA_HOME:+$JAVA_HOME/bin/}java" +if ! "$JAVA_BIN" --version 2>/dev/null | grep -q 'openjdk 17'; then + echo "gomobile Android runtime build requires OpenJDK 17." >&2 + "$JAVA_BIN" --version >&2 || true + exit 1 +fi + +if ! command -v go >/dev/null 2>&1; then + echo "go is required to build the gomobile runtime." >&2 + exit 1 +fi + +GOPATH="${GOPATH:-$(go env GOPATH)}" +if [[ $INSTALL_GOMOBILE -eq 1 ]]; then + needs_gomobile_install=0 + if [[ ! -x "$GOPATH/bin/gomobile" || ! -x "$GOPATH/bin/gobind" ]]; then + needs_gomobile_install=1 + elif ! "$GOPATH/bin/gomobile" bind -h 2>&1 | grep -q -- '-libname'; then + needs_gomobile_install=1 + fi + + if [[ $needs_gomobile_install -eq 1 ]]; then + go install -v "$GOMOBILE_MODULE/cmd/gomobile@$GOMOBILE_VERSION" + go install -v "$GOMOBILE_MODULE/cmd/gobind@$GOMOBILE_VERSION" + fi +fi + +"$GOPATH/bin/gomobile" init + +WORK_DIR="$RUNTIME_DIR/.work/android" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" "$(dirname "$OUTPUT_AAR")" + +cat > "$WORK_DIR/go.mod" < $SING_BOX_SOURCE +replace github.com/tladesignz/IPtProxy.git => $IPTPROXY_SOURCE/IPtProxy.go +replace www.bamsoftware.com/git/dnstt.git => $DNSTT_SOURCE +EOF + +cat > "$WORK_DIR/tools.go" <<'EOF' +package tools + +import ( + _ "github.com/sagernet/gomobile" + _ "github.com/sagernet/gomobile/bind" + _ "github.com/sagernet/sing-box/experimental/libbox" + _ "github.com/tladesignz/IPtProxy.git" +) +EOF + +pushd "$WORK_DIR" >/dev/null +go get "$GOMOBILE_MODULE/bind@$GOMOBILE_VERSION" +go mod tidy + +args=( + bind + -v + -target "$TARGET" + -androidapi "$ANDROID_API" + -javapkg=io.nekohasekai + -libname=box + -o "$OUTPUT_AAR" + -trimpath + -tags "$GO_MOBILE_TAGS" +) + +if [[ $DEBUG -eq 0 ]]; then + args+=(-ldflags "-s -w -checklinkname=0") +else + args+=(-ldflags "-checklinkname=0") +fi + +args+=( + github.com/sagernet/sing-box/experimental/libbox + github.com/tladesignz/IPtProxy.git +) + +"$GOPATH/bin/gomobile" "${args[@]}" +popd >/dev/null + +echo "Installed $OUTPUT_AAR" +if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$OUTPUT_AAR" +fi diff --git a/native/go_mobile_runtime/scripts/fdroid-prebuild.sh b/native/go_mobile_runtime/scripts/fdroid-prebuild.sh new file mode 100755 index 00000000..635e014f --- /dev/null +++ b/native/go_mobile_runtime/scripts/fdroid-prebuild.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" + +"$SCRIPT_DIR/build-android.sh" "$@" diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoContainerProxyApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoContainerProxyApiImpl.kt index eae80190..f758363d 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoContainerProxyApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoContainerProxyApiImpl.kt @@ -10,6 +10,7 @@ import eu.weblibre.flutter_mozilla_components.feature.BrowserExtensionFeature import eu.weblibre.flutter_mozilla_components.feature.ContainerProxyFeature import eu.weblibre.flutter_mozilla_components.feature.ResultConsumer import eu.weblibre.flutter_mozilla_components.pigeons.GeckoContainerProxyApi +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoProxySettings import org.json.JSONObject class GeckoContainerProxyApiImpl : GeckoContainerProxyApi { @@ -25,6 +26,38 @@ class GeckoContainerProxyApiImpl : GeckoContainerProxyApi { ContainerProxyFeature.scheduleRequest("removeContainerProxy", contextId) } + override fun upsertProxy(proxy: GeckoProxySettings) { + ContainerProxyFeature.scheduleRequest("upsertProxy", proxy.toJson()) + } + + override fun removeProxy(proxyId: String) { + ContainerProxyFeature.scheduleRequest("removeProxy", proxyId) + } + + override fun setContainerProxy(contextId: String, proxyId: String) { + ContainerProxyFeature.scheduleRequest( + "setContainerProxy", + JSONObject().apply { + put("contextId", contextId) + put("proxyId", proxyId) + } + ) + } + + override fun clearContainerProxy(contextId: String) { + ContainerProxyFeature.scheduleRequest("clearContainerProxy", contextId) + } + + override fun removeContainerProxyRelation(contextId: String, proxyId: String) { + ContainerProxyFeature.scheduleRequest( + "removeContainerProxyRelation", + JSONObject().apply { + put("contextId", contextId) + put("proxyId", proxyId) + } + ) + } + override fun setSiteAssignments(assignments: Map) { ContainerProxyFeature.scheduleRequest("setSiteAssignments", JSONObject(assignments)) } @@ -42,4 +75,18 @@ class GeckoContainerProxyApiImpl : GeckoContainerProxyApi { } }) } -} \ No newline at end of file + + private fun GeckoProxySettings.toJson(): JSONObject { + return JSONObject().apply { + put("id", id) + put("title", title) + put("type", type) + put("host", host) + put("port", port) + username?.let { put("username", it) } + password?.let { put("password", it) } + put("proxyDNS", proxyDNS) + put("doNotProxyLocal", doNotProxyLocal) + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/interceptor/AppRequestInterceptor.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/interceptor/AppRequestInterceptor.kt index a5fb59d0..95304c57 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/interceptor/AppRequestInterceptor.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/interceptor/AppRequestInterceptor.kt @@ -12,9 +12,11 @@ import android.content.Intent import android.net.Uri import android.util.Log import eu.weblibre.flutter_mozilla_components.GlobalComponents +import eu.weblibre.flutter_mozilla_components.ext.EventSequence import eu.weblibre.flutter_mozilla_components.feature.InertExternalSchemes import eu.weblibre.flutter_mozilla_components.feature.SandboxCaptureBridge import eu.weblibre.flutter_mozilla_components.feature.SandboxCaptureRegistry +import eu.weblibre.flutter_mozilla_components.pigeons.ProxyLoadError import mozilla.components.browser.errorpages.ErrorPages import mozilla.components.browser.errorpages.ErrorType import mozilla.components.browser.state.selector.findTabOrCustomTab @@ -145,6 +147,21 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor { errorType: ErrorType, uri: String?, ): RequestInterceptor.ErrorResponse { + if (errorType == ErrorType.ERROR_PROXY_CONNECTION_REFUSED || + errorType == ErrorType.ERROR_UNKNOWN_PROXY_HOST + ) { + val tab = components.core.store.state.findTabOrCustomTab(session) + components.flutterEvents.onProxyLoadError( + EventSequence.next(), + ProxyLoadError( + tabId = tab?.id, + contextId = tab?.contextId, + url = uri, + errorType = errorType.name, + ) + ) { _ -> } + } + val errorPage = ErrorPages.createUrlEncodedErrorPage(context, errorType, uri) return RequestInterceptor.ErrorResponse(errorPage) } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index b89b843a..20a7367c 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -4573,6 +4573,72 @@ data class MlProgressData ( } } +/** Generated class from Pigeon that represents data sent in messages. */ +data class GeckoProxySettings ( + val id: String, + val title: String, + val type: String, + val host: String, + val port: Long, + val username: String? = null, + val password: String? = null, + val proxyDNS: Boolean, + val doNotProxyLocal: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): GeckoProxySettings { + val id = pigeonVar_list[0] as String + val title = pigeonVar_list[1] as String + val type = pigeonVar_list[2] as String + val host = pigeonVar_list[3] as String + val port = pigeonVar_list[4] as Long + val username = pigeonVar_list[5] as String? + val password = pigeonVar_list[6] as String? + val proxyDNS = pigeonVar_list[7] as Boolean + val doNotProxyLocal = pigeonVar_list[8] as Boolean + return GeckoProxySettings(id, title, type, host, port, username, password, proxyDNS, doNotProxyLocal) + } + } + fun toList(): List { + return listOf( + id, + title, + type, + host, + port, + username, + password, + proxyDNS, + doNotProxyLocal, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as GeckoProxySettings + return GeckoPigeonUtils.deepEquals(this.id, other.id) && GeckoPigeonUtils.deepEquals(this.title, other.title) && GeckoPigeonUtils.deepEquals(this.type, other.type) && GeckoPigeonUtils.deepEquals(this.host, other.host) && GeckoPigeonUtils.deepEquals(this.port, other.port) && GeckoPigeonUtils.deepEquals(this.username, other.username) && GeckoPigeonUtils.deepEquals(this.password, other.password) && GeckoPigeonUtils.deepEquals(this.proxyDNS, other.proxyDNS) && GeckoPigeonUtils.deepEquals(this.doNotProxyLocal, other.doNotProxyLocal) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + GeckoPigeonUtils.deepHash(this.id) + result = 31 * result + GeckoPigeonUtils.deepHash(this.title) + result = 31 * result + GeckoPigeonUtils.deepHash(this.type) + result = 31 * result + GeckoPigeonUtils.deepHash(this.host) + result = 31 * result + GeckoPigeonUtils.deepHash(this.port) + result = 31 * result + GeckoPigeonUtils.deepHash(this.username) + result = 31 * result + GeckoPigeonUtils.deepHash(this.password) + result = 31 * result + GeckoPigeonUtils.deepHash(this.proxyDNS) + result = 31 * result + GeckoPigeonUtils.deepHash(this.doNotProxyLocal) + return result + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class ContainerSiteAssignment ( val requestId: String, @@ -4623,6 +4689,52 @@ data class ContainerSiteAssignment ( } } +/** Generated class from Pigeon that represents data sent in messages. */ +data class ProxyLoadError ( + val tabId: String? = null, + val contextId: String? = null, + val url: String? = null, + val errorType: String +) + { + companion object { + fun fromList(pigeonVar_list: List): ProxyLoadError { + val tabId = pigeonVar_list[0] as String? + val contextId = pigeonVar_list[1] as String? + val url = pigeonVar_list[2] as String? + val errorType = pigeonVar_list[3] as String + return ProxyLoadError(tabId, contextId, url, errorType) + } + } + fun toList(): List { + return listOf( + tabId, + contextId, + url, + errorType, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as ProxyLoadError + return GeckoPigeonUtils.deepEquals(this.tabId, other.tabId) && GeckoPigeonUtils.deepEquals(this.contextId, other.contextId) && GeckoPigeonUtils.deepEquals(this.url, other.url) && GeckoPigeonUtils.deepEquals(this.errorType, other.errorType) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + GeckoPigeonUtils.deepHash(this.tabId) + result = 31 * result + GeckoPigeonUtils.deepHash(this.contextId) + result = 31 * result + GeckoPigeonUtils.deepHash(this.url) + result = 31 * result + GeckoPigeonUtils.deepHash(this.errorType) + return result + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class GeckoHeader ( val key: String, @@ -5995,75 +6107,85 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { } 235.toByte() -> { return (readValue(buffer) as? List)?.let { - ContainerSiteAssignment.fromList(it) + GeckoProxySettings.fromList(it) } } 236.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoHeader.fromList(it) + ContainerSiteAssignment.fromList(it) } } 237.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoFetchRequest.fromList(it) + ProxyLoadError.fromList(it) } } 238.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoFetchResponse.fromList(it) + GeckoHeader.fromList(it) } } 239.toByte() -> { return (readValue(buffer) as? List)?.let { - BookmarkNode.fromList(it) + GeckoFetchRequest.fromList(it) } } 240.toByte() -> { return (readValue(buffer) as? List)?.let { - BookmarkInfo.fromList(it) + GeckoFetchResponse.fromList(it) } } 241.toByte() -> { return (readValue(buffer) as? List)?.let { - SitePermissions.fromList(it) + BookmarkNode.fromList(it) } } 242.toByte() -> { return (readValue(buffer) as? List)?.let { - TrackingProtectionException.fromList(it) + BookmarkInfo.fromList(it) } } 243.toByte() -> { return (readValue(buffer) as? List)?.let { - PwaIcon.fromList(it) + SitePermissions.fromList(it) } } 244.toByte() -> { return (readValue(buffer) as? List)?.let { - ShareTargetFiles.fromList(it) + TrackingProtectionException.fromList(it) } } 245.toByte() -> { return (readValue(buffer) as? List)?.let { - ShareTargetParams.fromList(it) + PwaIcon.fromList(it) } } 246.toByte() -> { return (readValue(buffer) as? List)?.let { - ShareTarget.fromList(it) + ShareTargetFiles.fromList(it) } } 247.toByte() -> { return (readValue(buffer) as? List)?.let { - ExternalApplicationResource.fromList(it) + ShareTargetParams.fromList(it) } } 248.toByte() -> { return (readValue(buffer) as? List)?.let { - PwaManifest.fromList(it) + ShareTarget.fromList(it) } } 249.toByte() -> { + return (readValue(buffer) as? List)?.let { + ExternalApplicationResource.fromList(it) + } + } + 250.toByte() -> { + return (readValue(buffer) as? List)?.let { + PwaManifest.fromList(it) + } + } + 251.toByte() -> { return (readValue(buffer) as? List)?.let { SandboxCaptureEntry.fromList(it) } @@ -6497,66 +6619,74 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { stream.write(234) writeValue(stream, value.toList()) } - is ContainerSiteAssignment -> { + is GeckoProxySettings -> { stream.write(235) writeValue(stream, value.toList()) } - is GeckoHeader -> { + is ContainerSiteAssignment -> { stream.write(236) writeValue(stream, value.toList()) } - is GeckoFetchRequest -> { + is ProxyLoadError -> { stream.write(237) writeValue(stream, value.toList()) } - is GeckoFetchResponse -> { + is GeckoHeader -> { stream.write(238) writeValue(stream, value.toList()) } - is BookmarkNode -> { + is GeckoFetchRequest -> { stream.write(239) writeValue(stream, value.toList()) } - is BookmarkInfo -> { + is GeckoFetchResponse -> { stream.write(240) writeValue(stream, value.toList()) } - is SitePermissions -> { + is BookmarkNode -> { stream.write(241) writeValue(stream, value.toList()) } - is TrackingProtectionException -> { + is BookmarkInfo -> { stream.write(242) writeValue(stream, value.toList()) } - is PwaIcon -> { + is SitePermissions -> { stream.write(243) writeValue(stream, value.toList()) } - is ShareTargetFiles -> { + is TrackingProtectionException -> { stream.write(244) writeValue(stream, value.toList()) } - is ShareTargetParams -> { + is PwaIcon -> { stream.write(245) writeValue(stream, value.toList()) } - is ShareTarget -> { + is ShareTargetFiles -> { stream.write(246) writeValue(stream, value.toList()) } - is ExternalApplicationResource -> { + is ShareTargetParams -> { stream.write(247) writeValue(stream, value.toList()) } - is PwaManifest -> { + is ShareTarget -> { stream.write(248) writeValue(stream, value.toList()) } - is SandboxCaptureEntry -> { + is ExternalApplicationResource -> { stream.write(249) writeValue(stream, value.toList()) } + is PwaManifest -> { + stream.write(250) + writeValue(stream, value.toList()) + } + is SandboxCaptureEntry -> { + stream.write(251) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -8279,6 +8409,11 @@ interface GeckoContainerProxyApi { fun setProxyPort(port: Long) fun addContainerProxy(contextId: String) fun removeContainerProxy(contextId: String) + fun upsertProxy(proxy: GeckoProxySettings) + fun removeProxy(proxyId: String) + fun setContainerProxy(contextId: String, proxyId: String) + fun clearContainerProxy(contextId: String) + fun removeContainerProxyRelation(contextId: String, proxyId: String) fun setSiteAssignments(assignments: Map) fun healthcheck(callback: (Result) -> Unit) @@ -8345,6 +8480,98 @@ interface GeckoContainerProxyApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.upsertProxy$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val proxyArg = args[0] as GeckoProxySettings + val wrapped: List = try { + api.upsertProxy(proxyArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.removeProxy$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val proxyIdArg = args[0] as String + val wrapped: List = try { + api.removeProxy(proxyIdArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setContainerProxy$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val contextIdArg = args[0] as String + val proxyIdArg = args[1] as String + val wrapped: List = try { + api.setContainerProxy(contextIdArg, proxyIdArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.clearContainerProxy$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val contextIdArg = args[0] as String + val wrapped: List = try { + api.clearContainerProxy(contextIdArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.removeContainerProxyRelation$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val contextIdArg = args[0] as String + val proxyIdArg = args[1] as String + val wrapped: List = try { + api.removeContainerProxyRelation(contextIdArg, proxyIdArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setSiteAssignments$separatedMessageChannelSuffix", codec) if (api != null) { @@ -8785,6 +9012,23 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } + fun onProxyLoadError(sequenceArg: Long, detailsArg: ProxyLoadError, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onProxyLoadError$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(sequenceArg, detailsArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName))) + } + } + } fun onMlProgress(sequenceArg: Long, progressArg: MlProgressData, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" diff --git a/packages/flutter_mozilla_components/javascript/container_proxy/src/background/BackgroundMain.ts b/packages/flutter_mozilla_components/javascript/container_proxy/src/background/BackgroundMain.ts index 9b0f6800..00858469 100644 --- a/packages/flutter_mozilla_components/javascript/container_proxy/src/background/BackgroundMain.ts +++ b/packages/flutter_mozilla_components/javascript/container_proxy/src/background/BackgroundMain.ts @@ -15,7 +15,7 @@ const privateIdentifier = 'firefox-private' type DoNotProxy = never[] export const doNotProxy: DoNotProxy = [] -const emergencyBreak: Socks5ProxyInfo = { +export const emergencyBreak: Socks5ProxyInfo = { type: ProxyType.Socks5, host: 'emergency-break-proxy.localhost', port: 1, diff --git a/packages/flutter_mozilla_components/javascript/container_proxy/src/background/index.ts b/packages/flutter_mozilla_components/javascript/container_proxy/src/background/index.ts index 4d636a7c..362de3ba 100644 --- a/packages/flutter_mozilla_components/javascript/container_proxy/src/background/index.ts +++ b/packages/flutter_mozilla_components/javascript/container_proxy/src/background/index.ts @@ -1,4 +1,4 @@ -import { Socks5ProxySettings } from 'src/domain/ProxySettings'; +import { ProxySettings, Socks5ProxySettings } from 'src/domain/ProxySettings'; import { Store } from '../store/Store' import BackgroundMain from './BackgroundMain' @@ -8,7 +8,16 @@ const store = new Store() interface Message { id: String | undefined; - action: 'setProxyPort' | 'addContainerProxy' | 'removeContainerProxy' | 'healthcheck' | 'setSiteAssignments'; + action: 'setProxyPort' | + 'addContainerProxy' | + 'removeContainerProxy' | + 'upsertProxy' | + 'removeProxy' | + 'setContainerProxy' | + 'clearContainerProxy' | + 'removeContainerProxyRelation' | + 'healthcheck' | + 'setSiteAssignments'; args: any; } @@ -42,6 +51,32 @@ port.onMessage.addListener((raw: unknown): void => { store.removeContainerProxyRelation(message.args, "tor") console.log('removed container relation ' + message.args) break + case "upsertProxy": { + const proxy = ProxySettings.tryFromDao(message.args) + if (proxy === undefined) { + console.error('invalid proxy settings ' + JSON.stringify(message.args)) + break + } + store.putProxy(proxy) + console.log('upsert proxy ' + message.args.id) + break + } + case "removeProxy": + store.deleteProxyById(message.args) + console.log('removed proxy ' + message.args) + break + case "setContainerProxy": + store.setContainerProxyRelation(message.args.contextId, message.args.proxyId) + console.log('set container relation ' + message.args.contextId + ' -> ' + message.args.proxyId) + break + case "clearContainerProxy": + store.clearContainerProxyRelation(message.args) + console.log('cleared container relation ' + message.args) + break + case "removeContainerProxyRelation": + store.removeContainerProxyRelation(message.args.contextId, message.args.proxyId) + console.log('removed container relation ' + message.args.contextId + ' -> ' + message.args.proxyId) + break case "setSiteAssignments": const entries = new Map(Object.entries(message.args)) console.log('set site assignments ' + JSON.stringify(message.args)) diff --git a/packages/flutter_mozilla_components/javascript/container_proxy/src/store/Store.ts b/packages/flutter_mozilla_components/javascript/container_proxy/src/store/Store.ts index 277ed0ee..8560f867 100644 --- a/packages/flutter_mozilla_components/javascript/container_proxy/src/store/Store.ts +++ b/packages/flutter_mozilla_components/javascript/container_proxy/src/store/Store.ts @@ -224,6 +224,10 @@ export class Store { this.relations[cookieStoreId] = [proxyId] } + clearContainerProxyRelation(cookieStoreId: string): void { + delete this.relations[cookieStoreId] + } + removeContainerProxyRelation(cookieStoreId: string, proxyId: string): void { const currentRelations = this.relations[cookieStoreId] ?? [] this.relations[cookieStoreId] = currentRelations.filter(id => id !== proxyId) diff --git a/packages/flutter_mozilla_components/javascript/container_proxy/test/unit/BackgroundMain.test.ts b/packages/flutter_mozilla_components/javascript/container_proxy/test/unit/BackgroundMain.test.ts index 0da3e27e..e109876d 100644 --- a/packages/flutter_mozilla_components/javascript/container_proxy/test/unit/BackgroundMain.test.ts +++ b/packages/flutter_mozilla_components/javascript/container_proxy/test/unit/BackgroundMain.test.ts @@ -1,4 +1,4 @@ -import BackgroundMain, { doNotProxy } from '../../src/background/BackgroundMain' +import BackgroundMain, { doNotProxy, emergencyBreak } from '../../src/background/BackgroundMain' import { Store } from '../../src/store/Store' import { expect } from 'chai' @@ -41,6 +41,16 @@ describe('BackgroundMain', function () { expect(result).to.be.not.empty }) + it('should block if an assigned proxy no longer exists', async () => { + const isolatedStore = new Store() + const isolatedBackgroundMain = new BackgroundMain({ store: isolatedStore }) + isolatedStore.setContainerProxyRelation('general', 'missing-proxy') + + const result = await isolatedBackgroundMain.onRequest({ cookieStoreId: 'firefox-default', url: 'https://google.com', tabId: 0 }) + + expect(result).to.be.deep.equal([emergencyBreak]) + }) + it('should remove doNotProxyLocal flag from proxy settings if proxy is set up', async () => { await givenSomeProxyIsSetUpForContainer({ containerId: 'firefox-default', host: undefined, doNotProxyLocal: undefined }) diff --git a/packages/flutter_mozilla_components/javascript/container_proxy/test/unit/Store.test.ts b/packages/flutter_mozilla_components/javascript/container_proxy/test/unit/Store.test.ts index 991909ac..a7022d6c 100644 --- a/packages/flutter_mozilla_components/javascript/container_proxy/test/unit/Store.test.ts +++ b/packages/flutter_mozilla_components/javascript/container_proxy/test/unit/Store.test.ts @@ -163,6 +163,15 @@ describe('Store', () => { expect(relations.container1).to.be.deep.equal(['proxy1']) expect(relations.container2).to.be.deep.equal(['proxy2']) }) + + it('should keep a missing proxy relation distinguishable from no relation', () => { + const isolatedStore = new Store() + isolatedStore.setContainerProxyRelation('container1', 'deleted-proxy') + + const result = isolatedStore.getProxiesForContainer('container1') + + expect(result).to.be.deep.equal([]) + }) }) describe('wildcard site assignments', function () { diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index 5051f7ed..41b3caf1 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -69,6 +69,7 @@ export 'src/pigeons/gecko.g.dart' GeckoDeleteBrowsingDataController, GeckoEngineSettings, GeckoFetchResponse, + GeckoProxySettings, GeckoPref, GeckoPublicSuffixListApi, GeckoPwaApi, @@ -93,6 +94,7 @@ export 'src/pigeons/gecko.g.dart' MlProgressStatus, MlProgressType, PhoneHitResult, + ProxyLoadError, PwaIcon, PwaManifest, QueryParameterStripping, diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_container_proxy.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_container_proxy.dart index 3667efd3..6a17abd5 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_container_proxy.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_container_proxy.dart @@ -24,6 +24,26 @@ class GeckoContainerProxyService { return _apiInstance.removeContainerProxy(contextId); } + Future upsertProxy(GeckoProxySettings proxy) { + return _apiInstance.upsertProxy(proxy); + } + + Future removeProxy(String proxyId) { + return _apiInstance.removeProxy(proxyId); + } + + Future setContainerProxy(String contextId, String proxyId) { + return _apiInstance.setContainerProxy(contextId, proxyId); + } + + Future clearContainerProxy(String contextId) { + return _apiInstance.clearContainerProxy(contextId); + } + + Future removeContainerProxyRelation(String contextId, String proxyId) { + return _apiInstance.removeContainerProxyRelation(contextId, proxyId); + } + Future setSiteAssignments(Map assignments) { return _apiInstance.setSiteAssignments(assignments); } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart index ccffa091..c07bdf0c 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart @@ -44,6 +44,7 @@ class GeckoEventService extends GeckoStateEvents { // final _scrollEventSubject = PublishSubject(); final _prefUpdateSubject = PublishSubject(); final _siteAssignementSubject = PublishSubject(); + final _proxyLoadErrorSubject = PublishSubject(); final _tabAddedSubject = PublishSubject(); final _mlProgressSubject = PublishSubject(); @@ -71,6 +72,8 @@ class GeckoEventService extends GeckoStateEvents { Stream get prefUpdateEvent => _prefUpdateSubject.stream; Stream get siteAssignementEvent => _siteAssignementSubject.stream; + Stream get proxyLoadErrorEvents => + _proxyLoadErrorSubject.stream; Stream get tabAddedStream => _tabAddedSubject.stream; Stream get mlProgressEvents => _mlProgressSubject.stream; @@ -205,6 +208,11 @@ class GeckoEventService extends GeckoStateEvents { ); } + @override + void onProxyLoadError(int sequence, ProxyLoadError details) { + _proxyLoadErrorSubject.addWhenMoreRecent(sequence, details.tabId, details); + } + @override void onMlProgress(int sequence, MlProgressData progress) { _mlProgressSubject.addWhenMoreRecent(sequence, null, progress); @@ -266,6 +274,7 @@ class GeckoEventService extends GeckoStateEvents { await _tabAddedSubject.close(); await _prefUpdateSubject.close(); await _siteAssignementSubject.close(); + await _proxyLoadErrorSubject.close(); await _mlProgressSubject.close(); await _manifestUpdateSubject.close(); await _translationEngineSubject.close(); diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_history.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_history.dart index ae1b6dac..528603b6 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_history.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_history.dart @@ -91,7 +91,10 @@ class GeckoHistoryService { return _api.getVisited(urls); } - Future> getSuggestions(String query, {int limit = 10}) { + Future> getSuggestions( + String query, { + int limit = 10, + }) { return _api.getSuggestions(query, limit); } @@ -140,6 +143,8 @@ class GeckoHistoryService { } Future deleteHistoryMetadataOlderThan(DateTime olderThan) { - return _api.deleteHistoryMetadataOlderThan(olderThan.millisecondsSinceEpoch); + return _api.deleteHistoryMetadataOlderThan( + olderThan.millisecondsSinceEpoch, + ); } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 2ba1bb29..3de43ea1 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -10,9 +10,9 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; Object? _extractReplyValueOrThrow( - List? replyList, - String channelName, { - required bool isNullValid, + List? replyList, + String channelName, { + required bool isNullValid, }) { if (replyList == null) { throw PlatformException( @@ -34,8 +34,11 @@ Object? _extractReplyValueOrThrow( return replyList.firstOrNull; } - -List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { +List wrapResponse({ + Object? result, + PlatformException? error, + bool empty = false, +}) { if (empty) { return []; } @@ -44,6 +47,7 @@ List wrapResponse({Object? result, PlatformException? error, bool empty } return [error.code, error.message, error.details]; } + bool _deepEquals(Object? a, Object? b) { if (identical(a, b)) { return true; @@ -56,8 +60,9 @@ bool _deepEquals(Object? a, Object? b) { } if (a is List && b is List) { return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); } if (a is Map && b is Map) { if (a.length != b.length) { @@ -106,13 +111,14 @@ int _deepHash(Object? value) { return value.hashCode; } - /// Indicates what location the tabs should be restored at enum RestoreLocation { /// Restore tabs at the beginning of the tab list beginning, + /// Restore tabs at the end of the tab list end, + /// Restore tabs at a specific index in the tab list atIndex, } @@ -133,80 +139,71 @@ enum IconType { /// Supported sizes. /// /// We are trying to limit the supported sizes in order to optimize our caching strategy. -enum IconSize { - defaultSize, - launcher, - launcherAdaptive, -} +enum IconSize { defaultSize, launcher, launcherAdaptive } /// The source of an [Icon]. enum IconSource { /// This icon was generated. generator, + /// This icon was downloaded. download, + /// This icon was inlined in the document. inline, + /// This icon was loaded from an in-memory cache. memory, + /// This icon was loaded from a disk cache. disk, } -enum CookieSameSiteStatus { - noRestriction, - lax, - strict, - unspecified, -} +enum CookieSameSiteStatus { noRestriction, lax, strict, unspecified } enum VisitType { /// The user followed a link and got a new toplevel window. link, + /// The user typed the page's URL in the URL bar or selected it from /// URL bar autocomplete results, clicked on it from a history query /// (from the History sidebar, History menu, or history query in the /// personal toolbar or Places organizer. typed, + /// The user followed a bookmark to get to the page. bookmark, + /// Some inner content is loaded. This is true of all images on a /// page, and the contents of the iframe. It is also true of any /// content in a frame if the user did not explicitly follow a link /// to get there. embed, + /// Set when the transition was a permanent redirect. redirectPermanent, + /// Set when the transition was a temporary redirect. redirectTemporary, + /// Set when the transition is a download. download, + /// The user followed a link and got a visit in a frame. framedLink, + /// The user reloaded a page. reload, } -enum FrecencyThresholdOption { - none, - skipOneTimePages, -} +enum FrecencyThresholdOption { none, skipOneTimePages } /// Document type associated with a [HistoryMetadata] record. -enum DocumentType { - regular, - media, -} +enum DocumentType { regular, media } -enum SelectionPattern { - phone, - email, -} +enum SelectionPattern { phone, email } -enum WebExtensionActionType { - browser, - page, -} +enum WebExtensionActionType { browser, page } enum AddonDisabledReason { unsupported, @@ -217,11 +214,7 @@ enum AddonDisabledReason { softBlocked, } -enum AddonIncognito { - spanning, - split, - notAllowed, -} +enum AddonIncognito { spanning, split, notAllowed } enum AddonUpdateStatus { notInstalled, @@ -230,74 +223,47 @@ enum AddonUpdateStatus { error, } -enum AddonStoreApp { - android, - firefox, -} +enum AddonStoreApp { android, firefox } -enum AddonStorePromoted { - none, - recommended, - line, -} +enum AddonStorePromoted { none, recommended, line } -enum GeckoSuggestionType { - session, - clipboard, - history, -} +enum GeckoSuggestionType { session, clipboard, history } -enum TrackingProtectionPolicy { - none, - recommended, - strict, - custom, -} +enum TrackingProtectionPolicy { none, recommended, strict, custom } -enum HttpsOnlyMode { - disabled, - privateOnly, - enabled, -} +enum HttpsOnlyMode { disabled, privateOnly, enabled } -enum QueryParameterStripping { - disabled, - privateOnly, - enabled, -} +enum QueryParameterStripping { disabled, privateOnly, enabled } enum BounceTrackingProtectionMode { /// Fully disabled. disabled, + /// Fully enabled. enabled, + /// Disabled, but collects user interaction data. Use this mode as the /// "disabled" state when the feature can be toggled on and off, e.g. via /// preferences. enabledStandby, + /// Feature enabled, but tracker purging is only simulated. Used for /// testing and telemetry collection. enabledDryRun, } -enum ColorScheme { - system, - light, - dark, -} +enum ColorScheme { system, light, dark } -enum CookieBannerHandlingMode { - disabled, - rejectAll, - rejectOrAcceptAll, -} +enum CookieBannerHandlingMode { disabled, rejectAll, rejectOrAcceptAll } /// App links behavior mode - controls how external app links are handled enum AppLinksMode { /// Always open links in their native apps without prompting always, + /// Prompt user before opening in app (with "Always open" checkbox) ask, + /// Never open links in external apps, always use browser never, } @@ -314,15 +280,19 @@ enum CustomCookiePolicy { /// Total Cookie Protection - Dynamic First-Party Isolation (dFPI) /// Most private option, isolates cookies per site totalProtection, + /// Block cross-site and social media tracker cookies /// Allows most cookies but blocks tracking cookies crossSiteTrackers, + /// Block cookies from sites you haven't visited /// Balances privacy with functionality unvisited, + /// Block all third-party cookies /// Only allows first-party cookies thirdParty, + /// Block all cookies (may break many sites) allCookies, } @@ -331,108 +301,77 @@ enum CustomCookiePolicy { enum TrackingScope { /// Apply to all browsing (normal + private) all, + /// Apply only to private browsing tabs privateOnly, } -enum DohSettingsMode { - geckoDefault, - increased, - max, - off, -} +enum DohSettingsMode { geckoDefault, increased, max, off } /// Status that represents every state that a download can be in. enum DownloadStatus { /// Indicates that the download is in the first state after creation but not yet [DOWNLOADING]. initiated, + /// Indicates that an [INITIATED] download is now actively being downloaded. downloading, + /// Indicates that the download that has been [DOWNLOADING] has been paused. paused, + /// Indicates that the download that has been [DOWNLOADING] has been cancelled. cancelled, + /// Indicates that the download that has been [DOWNLOADING] has moved to failed because /// something unexpected has happened. failed, + /// Indicates that the [DOWNLOADING] download has been completed. completed, } -enum LogLevel { - debug, - info, - warn, - error, -} +enum LogLevel { debug, info, warn, error } -enum SyncEngineValue { - history, - bookmarks, - tabs, -} +enum SyncEngineValue { history, bookmarks, tabs } /// Type of ML model operation -enum MlProgressType { - downloading, - loadingFromCache, - runningInference, -} +enum MlProgressType { downloading, loadingFromCache, runningInference } /// Status of the ML operation -enum MlProgressStatus { - initiate, - sizeEstimate, - inProgress, - done, -} +enum MlProgressStatus { initiate, sizeEstimate, inProgress, done } /// Types of browsing data that can be cleared enum ClearDataType { /// Authentication sessions authSessions, + /// All site data (cookies, storage, etc.) /// WARNING: If this is set it already includes cookies and allCaches. Passing the additionally will lead to issues allSiteData, + /// Cookies only onlyCookies, + /// Cache only onlyCaches, } -enum GeckoFetchMethod { - get, - head, - post, - put, - delete, - connect, - options, - trace, -} +enum GeckoFetchMethod { get, head, post, put, delete, connect, options, trace } -enum GeckoFetchRedircet { - follow, - manual, -} +enum GeckoFetchRedircet { follow, manual } -enum GeckoFetchCookiePolicy { - include, - omit, -} +enum GeckoFetchCookiePolicy { include, omit } -enum BookmarkNodeType { - item, - folder, - separator, -} +enum BookmarkNodeType { item, folder, separator } /// Permission status for a site permission enum SitePermissionStatus { /// Permission has been granted allowed, + /// Permission has been denied blocked, + /// No decision has been made yet (ask to allow) noDecision, } @@ -441,10 +380,13 @@ enum SitePermissionStatus { enum AutoplayStatus { /// Allow all autoplay (audible and inaudible) allowed, + /// Block all autoplay blocked, + /// Block audible autoplay only (allow inaudible) blockAudible, + /// Allow autoplay on WiFi only allowOnWifi, } @@ -454,26 +396,21 @@ enum AutoplayStatus { /// @property downloadModel If the necessary models should be downloaded on request. If false, then /// the translation will not complete and throw an exception if the models are not already available. class TranslationOptions { - TranslationOptions({ - required this.downloadModel, - }); + TranslationOptions({required this.downloadModel}); bool downloadModel; List _toList() { - return [ - downloadModel, - ]; + return [downloadModel]; } Object encode() { - return _toList(); } + return _toList(); + } static TranslationOptions decode(Object result) { result as List; - return TranslationOptions( - downloadModel: result[0]! as bool, - ); + return TranslationOptions(downloadModel: result[0]! as bool); } @override @@ -495,24 +432,19 @@ class TranslationOptions { /// A language supported by the translation engine. class TranslationLanguage { - TranslationLanguage({ - required this.code, - required this.localizedDisplayName, - }); + TranslationLanguage({required this.code, required this.localizedDisplayName}); String code; String localizedDisplayName; List _toList() { - return [ - code, - localizedDisplayName, - ]; + return [code, localizedDisplayName]; } Object encode() { - return _toList(); } + return _toList(); + } static TranslationLanguage decode(Object result) { result as List; @@ -531,7 +463,8 @@ class TranslationLanguage { if (identical(this, other)) { return true; } - return _deepEquals(code, other.code) && _deepEquals(localizedDisplayName, other.localizedDisplayName); + return _deepEquals(code, other.code) && + _deepEquals(localizedDisplayName, other.localizedDisplayName); } @override @@ -562,7 +495,8 @@ class TranslationDetectedLanguages { } Object encode() { - return _toList(); } + return _toList(); + } static TranslationDetectedLanguages decode(Object result) { result as List; @@ -576,13 +510,16 @@ class TranslationDetectedLanguages { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! TranslationDetectedLanguages || other.runtimeType != runtimeType) { + if (other is! TranslationDetectedLanguages || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { return true; } - return _deepEquals(documentLangTag, other.documentLangTag) && _deepEquals(supportedDocumentLang, other.supportedDocumentLang) && _deepEquals(userPreferredLangTag, other.userPreferredLangTag); + return _deepEquals(documentLangTag, other.documentLangTag) && + _deepEquals(supportedDocumentLang, other.supportedDocumentLang) && + _deepEquals(userPreferredLangTag, other.userPreferredLangTag); } @override @@ -592,24 +529,19 @@ class TranslationDetectedLanguages { /// A from/to language pair for translation. class TranslationPair { - TranslationPair({ - required this.fromLanguage, - required this.toLanguage, - }); + TranslationPair({required this.fromLanguage, required this.toLanguage}); String fromLanguage; String toLanguage; List _toList() { - return [ - fromLanguage, - toLanguage, - ]; + return [fromLanguage, toLanguage]; } Object encode() { - return _toList(); } + return _toList(); + } static TranslationPair decode(Object result) { result as List; @@ -628,7 +560,8 @@ class TranslationPair { if (identical(this, other)) { return true; } - return _deepEquals(fromLanguage, other.fromLanguage) && _deepEquals(toLanguage, other.toLanguage); + return _deepEquals(fromLanguage, other.fromLanguage) && + _deepEquals(toLanguage, other.toLanguage); } @override @@ -651,21 +584,19 @@ class TranslationEngineStateData { List? toLanguages; List _toList() { - return [ - isEngineSupported, - fromLanguages, - toLanguages, - ]; + return [isEngineSupported, fromLanguages, toLanguages]; } Object encode() { - return _toList(); } + return _toList(); + } static TranslationEngineStateData decode(Object result) { result as List; return TranslationEngineStateData( isEngineSupported: result[0] as bool?, - fromLanguages: (result[1] as List?)?.cast(), + fromLanguages: (result[1] as List?) + ?.cast(), toLanguages: (result[2] as List?)?.cast(), ); } @@ -673,13 +604,16 @@ class TranslationEngineStateData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! TranslationEngineStateData || other.runtimeType != runtimeType) { + if (other is! TranslationEngineStateData || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { return true; } - return _deepEquals(isEngineSupported, other.isEngineSupported) && _deepEquals(fromLanguages, other.fromLanguages) && _deepEquals(toLanguages, other.toLanguages); + return _deepEquals(isEngineSupported, other.isEngineSupported) && + _deepEquals(fromLanguages, other.fromLanguages) && + _deepEquals(toLanguages, other.toLanguages); } @override @@ -742,7 +676,8 @@ class TabTranslationStateData { } Object encode() { - return _toList(); } + return _toList(); + } static TabTranslationStateData decode(Object result) { result as List; @@ -770,7 +705,20 @@ class TabTranslationStateData { if (identical(this, other)) { return true; } - return _deepEquals(tabId, other.tabId) && _deepEquals(isTranslated, other.isTranslated) && _deepEquals(isTranslateProcessing, other.isTranslateProcessing) && _deepEquals(isOfferTranslate, other.isOfferTranslate) && _deepEquals(isExpectedTranslate, other.isExpectedTranslate) && _deepEquals(detectedLanguageCode, other.detectedLanguageCode) && _deepEquals(userPreferredLanguageCode, other.userPreferredLanguageCode) && _deepEquals(requestedFromLanguage, other.requestedFromLanguage) && _deepEquals(requestedToLanguage, other.requestedToLanguage) && _deepEquals(translationErrorName, other.translationErrorName) && _deepEquals(displayError, other.displayError); + return _deepEquals(tabId, other.tabId) && + _deepEquals(isTranslated, other.isTranslated) && + _deepEquals(isTranslateProcessing, other.isTranslateProcessing) && + _deepEquals(isOfferTranslate, other.isOfferTranslate) && + _deepEquals(isExpectedTranslate, other.isExpectedTranslate) && + _deepEquals(detectedLanguageCode, other.detectedLanguageCode) && + _deepEquals( + userPreferredLanguageCode, + other.userPreferredLanguageCode, + ) && + _deepEquals(requestedFromLanguage, other.requestedFromLanguage) && + _deepEquals(requestedToLanguage, other.requestedToLanguage) && + _deepEquals(translationErrorName, other.translationErrorName) && + _deepEquals(displayError, other.displayError); } @override @@ -828,7 +776,8 @@ class ReaderState { } Object encode() { - return _toList(); } + return _toList(); + } static ReaderState decode(Object result) { result as List; @@ -852,7 +801,13 @@ class ReaderState { if (identical(this, other)) { return true; } - return _deepEquals(readerable, other.readerable) && _deepEquals(active, other.active) && _deepEquals(checkRequired, other.checkRequired) && _deepEquals(connectRequired, other.connectRequired) && _deepEquals(baseUrl, other.baseUrl) && _deepEquals(activeUrl, other.activeUrl) && _deepEquals(scrollY, other.scrollY); + return _deepEquals(readerable, other.readerable) && + _deepEquals(active, other.active) && + _deepEquals(checkRequired, other.checkRequired) && + _deepEquals(connectRequired, other.connectRequired) && + _deepEquals(baseUrl, other.baseUrl) && + _deepEquals(activeUrl, other.activeUrl) && + _deepEquals(scrollY, other.scrollY); } @override @@ -907,7 +862,8 @@ class AddTabParams { } Object encode() { - return _toList(); } + return _toList(); + } static AddTabParams decode(Object result) { result as List; @@ -920,7 +876,8 @@ class AddTabParams { source: result[5]! as SourceValue, private: result[6]! as bool, historyMetadata: result[7] as HistoryMetadataKey?, - additionalHeaders: (result[8] as Map?)?.cast(), + additionalHeaders: (result[8] as Map?) + ?.cast(), ); } @@ -933,7 +890,15 @@ class AddTabParams { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(startLoading, other.startLoading) && _deepEquals(parentId, other.parentId) && _deepEquals(flags, other.flags) && _deepEquals(contextId, other.contextId) && _deepEquals(source, other.source) && _deepEquals(private, other.private) && _deepEquals(historyMetadata, other.historyMetadata) && _deepEquals(additionalHeaders, other.additionalHeaders); + return _deepEquals(url, other.url) && + _deepEquals(startLoading, other.startLoading) && + _deepEquals(parentId, other.parentId) && + _deepEquals(flags, other.flags) && + _deepEquals(contextId, other.contextId) && + _deepEquals(source, other.source) && + _deepEquals(private, other.private) && + _deepEquals(historyMetadata, other.historyMetadata) && + _deepEquals(additionalHeaders, other.additionalHeaders); } @override @@ -968,15 +933,12 @@ class LastMediaAccessState { bool mediaSessionActive; List _toList() { - return [ - lastMediaUrl, - lastMediaAccess, - mediaSessionActive, - ]; + return [lastMediaUrl, lastMediaAccess, mediaSessionActive]; } Object encode() { - return _toList(); } + return _toList(); + } static LastMediaAccessState decode(Object result) { result as List; @@ -996,7 +958,9 @@ class LastMediaAccessState { if (identical(this, other)) { return true; } - return _deepEquals(lastMediaUrl, other.lastMediaUrl) && _deepEquals(lastMediaAccess, other.lastMediaAccess) && _deepEquals(mediaSessionActive, other.mediaSessionActive); + return _deepEquals(lastMediaUrl, other.lastMediaUrl) && + _deepEquals(lastMediaAccess, other.lastMediaAccess) && + _deepEquals(mediaSessionActive, other.mediaSessionActive); } @override @@ -1009,11 +973,7 @@ class LastMediaAccessState { /// created, depending on the de-bouncing logic of the underlying storage i.e. recording history /// metadata observations with the exact same values may be combined into a single record. class HistoryMetadataKey { - HistoryMetadataKey({ - required this.url, - this.searchTerm, - this.referrerUrl, - }); + HistoryMetadataKey({required this.url, this.searchTerm, this.referrerUrl}); /// A url of the page. String url; @@ -1028,15 +988,12 @@ class HistoryMetadataKey { String? referrerUrl; List _toList() { - return [ - url, - searchTerm, - referrerUrl, - ]; + return [url, searchTerm, referrerUrl]; } Object encode() { - return _toList(); } + return _toList(); + } static HistoryMetadataKey decode(Object result) { result as List; @@ -1056,7 +1013,9 @@ class HistoryMetadataKey { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(searchTerm, other.searchTerm) && _deepEquals(referrerUrl, other.referrerUrl); + return _deepEquals(url, other.url) && + _deepEquals(searchTerm, other.searchTerm) && + _deepEquals(referrerUrl, other.referrerUrl); } @override @@ -1065,26 +1024,21 @@ class HistoryMetadataKey { } class PackageCategoryValue { - PackageCategoryValue({ - required this.value, - }); + PackageCategoryValue({required this.value}); int value; List _toList() { - return [ - value, - ]; + return [value]; } Object encode() { - return _toList(); } + return _toList(); + } static PackageCategoryValue decode(Object result) { result as List; - return PackageCategoryValue( - value: result[0]! as int, - ); + return PackageCategoryValue(value: result[0]! as int); } @override @@ -1106,10 +1060,7 @@ class PackageCategoryValue { /// Describes an external package. class ExternalPackage { - ExternalPackage({ - required this.packageId, - required this.category, - }); + ExternalPackage({required this.packageId, required this.category}); /// An Android package id. String packageId; @@ -1118,14 +1069,12 @@ class ExternalPackage { PackageCategoryValue category; List _toList() { - return [ - packageId, - category, - ]; + return [packageId, category]; } Object encode() { - return _toList(); } + return _toList(); + } static ExternalPackage decode(Object result) { result as List; @@ -1144,7 +1093,8 @@ class ExternalPackage { if (identical(this, other)) { return true; } - return _deepEquals(packageId, other.packageId) && _deepEquals(category, other.category); + return _deepEquals(packageId, other.packageId) && + _deepEquals(category, other.category); } @override @@ -1153,26 +1103,21 @@ class ExternalPackage { } class LoadUrlFlagsValue { - LoadUrlFlagsValue({ - required this.value, - }); + LoadUrlFlagsValue({required this.value}); int value; List _toList() { - return [ - value, - ]; + return [value]; } Object encode() { - return _toList(); } + return _toList(); + } static LoadUrlFlagsValue decode(Object result) { result as List; - return LoadUrlFlagsValue( - value: result[0]! as int, - ); + return LoadUrlFlagsValue(value: result[0]! as int); } @override @@ -1193,24 +1138,19 @@ class LoadUrlFlagsValue { } class SourceValue { - SourceValue({ - required this.id, - this.caller, - }); + SourceValue({required this.id, this.caller}); int id; ExternalPackage? caller; List _toList() { - return [ - id, - caller, - ]; + return [id, caller]; } Object encode() { - return _toList(); } + return _toList(); + } static SourceValue decode(Object result) { result as List; @@ -1329,7 +1269,8 @@ class TabState { } Object encode() { - return _toList(); } + return _toList(); + } static TabState decode(Object result) { result as List; @@ -1361,7 +1302,21 @@ class TabState { if (identical(this, other)) { return true; } - return _deepEquals(id, other.id) && _deepEquals(url, other.url) && _deepEquals(parentId, other.parentId) && _deepEquals(title, other.title) && _deepEquals(searchTerm, other.searchTerm) && _deepEquals(contextId, other.contextId) && _deepEquals(readerState, other.readerState) && _deepEquals(lastAccess, other.lastAccess) && _deepEquals(createdAt, other.createdAt) && _deepEquals(lastMediaAccessState, other.lastMediaAccessState) && _deepEquals(private, other.private) && _deepEquals(historyMetadata, other.historyMetadata) && _deepEquals(source, other.source) && _deepEquals(index, other.index) && _deepEquals(hasFormData, other.hasFormData); + return _deepEquals(id, other.id) && + _deepEquals(url, other.url) && + _deepEquals(parentId, other.parentId) && + _deepEquals(title, other.title) && + _deepEquals(searchTerm, other.searchTerm) && + _deepEquals(contextId, other.contextId) && + _deepEquals(readerState, other.readerState) && + _deepEquals(lastAccess, other.lastAccess) && + _deepEquals(createdAt, other.createdAt) && + _deepEquals(lastMediaAccessState, other.lastMediaAccessState) && + _deepEquals(private, other.private) && + _deepEquals(historyMetadata, other.historyMetadata) && + _deepEquals(source, other.source) && + _deepEquals(index, other.index) && + _deepEquals(hasFormData, other.hasFormData); } @override @@ -1371,10 +1326,7 @@ class TabState { /// A recoverable version of [TabState]. class RecoverableTab { - RecoverableTab({ - this.engineSessionStateJson, - required this.state, - }); + RecoverableTab({this.engineSessionStateJson, required this.state}); /// The [EngineSessionState] needed for restoring the previous state of this tab. String? engineSessionStateJson; @@ -1383,14 +1335,12 @@ class RecoverableTab { TabState state; List _toList() { - return [ - engineSessionStateJson, - state, - ]; + return [engineSessionStateJson, state]; } Object encode() { - return _toList(); } + return _toList(); + } static RecoverableTab decode(Object result) { result as List; @@ -1409,7 +1359,8 @@ class RecoverableTab { if (identical(this, other)) { return true; } - return _deepEquals(engineSessionStateJson, other.engineSessionStateJson) && _deepEquals(state, other.state); + return _deepEquals(engineSessionStateJson, other.engineSessionStateJson) && + _deepEquals(state, other.state); } @override @@ -1441,18 +1392,12 @@ class IconRequest { bool waitOnNetworkLoad; List _toList() { - return [ - url, - size, - resources, - color, - isPrivate, - waitOnNetworkLoad, - ]; + return [url, size, resources, color, isPrivate, waitOnNetworkLoad]; } Object encode() { - return _toList(); } + return _toList(); + } static IconRequest decode(Object result) { result as List; @@ -1475,7 +1420,12 @@ class IconRequest { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(size, other.size) && _deepEquals(resources, other.resources) && _deepEquals(color, other.color) && _deepEquals(isPrivate, other.isPrivate) && _deepEquals(waitOnNetworkLoad, other.waitOnNetworkLoad); + return _deepEquals(url, other.url) && + _deepEquals(size, other.size) && + _deepEquals(resources, other.resources) && + _deepEquals(color, other.color) && + _deepEquals(isPrivate, other.isPrivate) && + _deepEquals(waitOnNetworkLoad, other.waitOnNetworkLoad); } @override @@ -1484,31 +1434,23 @@ class IconRequest { } class ResourceSize { - ResourceSize({ - required this.height, - required this.width, - }); + ResourceSize({required this.height, required this.width}); int height; int width; List _toList() { - return [ - height, - width, - ]; + return [height, width]; } Object encode() { - return _toList(); } + return _toList(); + } static ResourceSize decode(Object result) { result as List; - return ResourceSize( - height: result[0]! as int, - width: result[1]! as int, - ); + return ResourceSize(height: result[0]! as int, width: result[1]! as int); } @override @@ -1549,17 +1491,12 @@ class Resource { bool maskable; List _toList() { - return [ - url, - type, - sizes, - mimeType, - maskable, - ]; + return [url, type, sizes, mimeType, maskable]; } Object encode() { - return _toList(); } + return _toList(); + } static Resource decode(Object result) { result as List; @@ -1581,7 +1518,11 @@ class Resource { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(type, other.type) && _deepEquals(sizes, other.sizes) && _deepEquals(mimeType, other.mimeType) && _deepEquals(maskable, other.maskable); + return _deepEquals(url, other.url) && + _deepEquals(type, other.type) && + _deepEquals(sizes, other.sizes) && + _deepEquals(mimeType, other.mimeType) && + _deepEquals(maskable, other.maskable); } @override @@ -1611,16 +1552,12 @@ class IconResult { bool maskable; List _toList() { - return [ - image, - color, - source, - maskable, - ]; + return [image, color, source, maskable]; } Object encode() { - return _toList(); } + return _toList(); + } static IconResult decode(Object result) { result as List; @@ -1641,7 +1578,10 @@ class IconResult { if (identical(this, other)) { return true; } - return _deepEquals(image, other.image) && _deepEquals(color, other.color) && _deepEquals(source, other.source) && _deepEquals(maskable, other.maskable); + return _deepEquals(image, other.image) && + _deepEquals(color, other.color) && + _deepEquals(source, other.source) && + _deepEquals(maskable, other.maskable); } @override @@ -1650,26 +1590,21 @@ class IconResult { } class CookiePartitionKey { - CookiePartitionKey({ - required this.topLevelSite, - }); + CookiePartitionKey({required this.topLevelSite}); String topLevelSite; List _toList() { - return [ - topLevelSite, - ]; + return [topLevelSite]; } Object encode() { - return _toList(); } + return _toList(); + } static CookiePartitionKey decode(Object result) { result as List; - return CookiePartitionKey( - topLevelSite: result[0]! as String, - ); + return CookiePartitionKey(topLevelSite: result[0]! as String); } @override @@ -1751,7 +1686,8 @@ class Cookie { } Object encode() { - return _toList(); } + return _toList(); + } static Cookie decode(Object result) { result as List; @@ -1781,7 +1717,19 @@ class Cookie { if (identical(this, other)) { return true; } - return _deepEquals(domain, other.domain) && _deepEquals(expirationDate, other.expirationDate) && _deepEquals(firstPartyDomain, other.firstPartyDomain) && _deepEquals(hostOnly, other.hostOnly) && _deepEquals(httpOnly, other.httpOnly) && _deepEquals(name, other.name) && _deepEquals(partitionKey, other.partitionKey) && _deepEquals(path, other.path) && _deepEquals(secure, other.secure) && _deepEquals(session, other.session) && _deepEquals(sameSite, other.sameSite) && _deepEquals(storeId, other.storeId) && _deepEquals(value, other.value); + return _deepEquals(domain, other.domain) && + _deepEquals(expirationDate, other.expirationDate) && + _deepEquals(firstPartyDomain, other.firstPartyDomain) && + _deepEquals(hostOnly, other.hostOnly) && + _deepEquals(httpOnly, other.httpOnly) && + _deepEquals(name, other.name) && + _deepEquals(partitionKey, other.partitionKey) && + _deepEquals(path, other.path) && + _deepEquals(secure, other.secure) && + _deepEquals(session, other.session) && + _deepEquals(sameSite, other.sameSite) && + _deepEquals(storeId, other.storeId) && + _deepEquals(value, other.value); } @override @@ -1827,7 +1775,8 @@ class VisitInfo { } Object encode() { - return _toList(); } + return _toList(); + } static VisitInfo decode(Object result) { result as List; @@ -1851,7 +1800,13 @@ class VisitInfo { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(title, other.title) && _deepEquals(visitTime, other.visitTime) && _deepEquals(visitType, other.visitType) && _deepEquals(previewImageUrl, other.previewImageUrl) && _deepEquals(isRemote, other.isRemote) && _deepEquals(contentId, other.contentId); + return _deepEquals(url, other.url) && + _deepEquals(title, other.title) && + _deepEquals(visitTime, other.visitTime) && + _deepEquals(visitType, other.visitType) && + _deepEquals(previewImageUrl, other.previewImageUrl) && + _deepEquals(isRemote, other.isRemote) && + _deepEquals(contentId, other.contentId); } @override @@ -1860,24 +1815,19 @@ class VisitInfo { } class HistoryHighlightWeights { - HistoryHighlightWeights({ - required this.viewTime, - required this.frequency, - }); + HistoryHighlightWeights({required this.viewTime, required this.frequency}); double viewTime; double frequency; List _toList() { - return [ - viewTime, - frequency, - ]; + return [viewTime, frequency]; } Object encode() { - return _toList(); } + return _toList(); + } static HistoryHighlightWeights decode(Object result) { result as List; @@ -1896,7 +1846,8 @@ class HistoryHighlightWeights { if (identical(this, other)) { return true; } - return _deepEquals(viewTime, other.viewTime) && _deepEquals(frequency, other.frequency); + return _deepEquals(viewTime, other.viewTime) && + _deepEquals(frequency, other.frequency); } @override @@ -1924,17 +1875,12 @@ class HistoryHighlight { String? previewImageUrl; List _toList() { - return [ - score, - placeId, - url, - title, - previewImageUrl, - ]; + return [score, placeId, url, title, previewImageUrl]; } Object encode() { - return _toList(); } + return _toList(); + } static HistoryHighlight decode(Object result) { result as List; @@ -1956,7 +1902,11 @@ class HistoryHighlight { if (identical(this, other)) { return true; } - return _deepEquals(score, other.score) && _deepEquals(placeId, other.placeId) && _deepEquals(url, other.url) && _deepEquals(title, other.title) && _deepEquals(previewImageUrl, other.previewImageUrl); + return _deepEquals(score, other.score) && + _deepEquals(placeId, other.placeId) && + _deepEquals(url, other.url) && + _deepEquals(title, other.title) && + _deepEquals(previewImageUrl, other.previewImageUrl); } @override @@ -1965,24 +1915,19 @@ class HistoryHighlight { } class TopFrecentSiteInfo { - TopFrecentSiteInfo({ - required this.url, - this.title, - }); + TopFrecentSiteInfo({required this.url, this.title}); String url; String? title; List _toList() { - return [ - url, - title, - ]; + return [url, title]; } Object encode() { - return _toList(); } + return _toList(); + } static TopFrecentSiteInfo decode(Object result) { result as List; @@ -2053,7 +1998,8 @@ class HistoryMetadata { } Object encode() { - return _toList(); } + return _toList(); + } static HistoryMetadata decode(Object result) { result as List; @@ -2077,7 +2023,13 @@ class HistoryMetadata { if (identical(this, other)) { return true; } - return _deepEquals(key, other.key) && _deepEquals(title, other.title) && _deepEquals(createdAt, other.createdAt) && _deepEquals(updatedAt, other.updatedAt) && _deepEquals(totalViewTime, other.totalViewTime) && _deepEquals(documentType, other.documentType) && _deepEquals(previewImageUrl, other.previewImageUrl); + return _deepEquals(key, other.key) && + _deepEquals(title, other.title) && + _deepEquals(createdAt, other.createdAt) && + _deepEquals(updatedAt, other.updatedAt) && + _deepEquals(totalViewTime, other.totalViewTime) && + _deepEquals(documentType, other.documentType) && + _deepEquals(previewImageUrl, other.previewImageUrl); } @override @@ -2087,11 +2039,7 @@ class HistoryMetadata { /// Frecency-ranked autocomplete suggestion. Backs `getSuggestions`. class HistorySuggestion { - HistorySuggestion({ - required this.url, - this.title, - required this.score, - }); + HistorySuggestion({required this.url, this.title, required this.score}); String url; @@ -2102,15 +2050,12 @@ class HistorySuggestion { int score; List _toList() { - return [ - url, - title, - score, - ]; + return [url, title, score]; } Object encode() { - return _toList(); } + return _toList(); + } static HistorySuggestion decode(Object result) { result as List; @@ -2130,7 +2075,9 @@ class HistorySuggestion { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(title, other.title) && _deepEquals(score, other.score); + return _deepEquals(url, other.url) && + _deepEquals(title, other.title) && + _deepEquals(score, other.score); } @override @@ -2140,24 +2087,19 @@ class HistorySuggestion { /// Optional metadata observation for a URL. `null` fields are not written. class PageObservation { - PageObservation({ - this.title, - this.previewImageUrl, - }); + PageObservation({this.title, this.previewImageUrl}); String? title; String? previewImageUrl; List _toList() { - return [ - title, - previewImageUrl, - ]; + return [title, previewImageUrl]; } Object encode() { - return _toList(); } + return _toList(); + } static PageObservation decode(Object result) { result as List; @@ -2176,7 +2118,8 @@ class PageObservation { if (identical(this, other)) { return true; } - return _deepEquals(title, other.title) && _deepEquals(previewImageUrl, other.previewImageUrl); + return _deepEquals(title, other.title) && + _deepEquals(previewImageUrl, other.previewImageUrl); } @override @@ -2185,31 +2128,23 @@ class PageObservation { } class HistoryItem { - HistoryItem({ - required this.url, - required this.title, - }); + HistoryItem({required this.url, required this.title}); String url; String title; List _toList() { - return [ - url, - title, - ]; + return [url, title]; } Object encode() { - return _toList(); } + return _toList(); + } static HistoryItem decode(Object result) { result as List; - return HistoryItem( - url: result[0]! as String, - title: result[1]! as String, - ); + return HistoryItem(url: result[0]! as String, title: result[1]! as String); } @override @@ -2246,16 +2181,12 @@ class HistoryState { bool canGoForward; List _toList() { - return [ - items, - currentIndex, - canGoBack, - canGoForward, - ]; + return [items, currentIndex, canGoBack, canGoForward]; } Object encode() { - return _toList(); } + return _toList(); + } static HistoryState decode(Object result) { result as List; @@ -2276,7 +2207,10 @@ class HistoryState { if (identical(this, other)) { return true; } - return _deepEquals(items, other.items) && _deepEquals(currentIndex, other.currentIndex) && _deepEquals(canGoBack, other.canGoBack) && _deepEquals(canGoForward, other.canGoForward); + return _deepEquals(items, other.items) && + _deepEquals(currentIndex, other.currentIndex) && + _deepEquals(canGoBack, other.canGoBack) && + _deepEquals(canGoForward, other.canGoForward); } @override @@ -2285,10 +2219,7 @@ class HistoryState { } class ReaderableState { - ReaderableState({ - required this.readerable, - required this.active, - }); + ReaderableState({required this.readerable, required this.active}); /// Whether or not the current page can be transformed to /// be displayed in a reader view. @@ -2298,14 +2229,12 @@ class ReaderableState { bool active; List _toList() { - return [ - readerable, - active, - ]; + return [readerable, active]; } Object encode() { - return _toList(); } + return _toList(); + } static ReaderableState decode(Object result) { result as List; @@ -2324,7 +2253,8 @@ class ReaderableState { if (identical(this, other)) { return true; } - return _deepEquals(readerable, other.readerable) && _deepEquals(active, other.active); + return _deepEquals(readerable, other.readerable) && + _deepEquals(active, other.active); } @override @@ -2346,15 +2276,12 @@ class SecurityInfoState { String issuer; List _toList() { - return [ - secure, - host, - issuer, - ]; + return [secure, host, issuer]; } Object encode() { - return _toList(); } + return _toList(); + } static SecurityInfoState decode(Object result) { result as List; @@ -2374,7 +2301,9 @@ class SecurityInfoState { if (identical(this, other)) { return true; } - return _deepEquals(secure, other.secure) && _deepEquals(host, other.host) && _deepEquals(issuer, other.issuer); + return _deepEquals(secure, other.secure) && + _deepEquals(host, other.host) && + _deepEquals(issuer, other.issuer); } @override @@ -2432,7 +2361,8 @@ class TabContentState { } Object encode() { - return _toList(); } + return _toList(); + } static TabContentState decode(Object result) { result as List; @@ -2459,7 +2389,16 @@ class TabContentState { if (identical(this, other)) { return true; } - return _deepEquals(id, other.id) && _deepEquals(parentId, other.parentId) && _deepEquals(contextId, other.contextId) && _deepEquals(url, other.url) && _deepEquals(title, other.title) && _deepEquals(progress, other.progress) && _deepEquals(isPrivate, other.isPrivate) && _deepEquals(isFullScreen, other.isFullScreen) && _deepEquals(isLoading, other.isLoading) && _deepEquals(showToolbarAsExpanded, other.showToolbarAsExpanded); + return _deepEquals(id, other.id) && + _deepEquals(parentId, other.parentId) && + _deepEquals(contextId, other.contextId) && + _deepEquals(url, other.url) && + _deepEquals(title, other.title) && + _deepEquals(progress, other.progress) && + _deepEquals(isPrivate, other.isPrivate) && + _deepEquals(isFullScreen, other.isFullScreen) && + _deepEquals(isLoading, other.isLoading) && + _deepEquals(showToolbarAsExpanded, other.showToolbarAsExpanded); } @override @@ -2481,15 +2420,12 @@ class FindResultState { bool isDoneCounting; List _toList() { - return [ - activeMatchOrdinal, - numberOfMatches, - isDoneCounting, - ]; + return [activeMatchOrdinal, numberOfMatches, isDoneCounting]; } Object encode() { - return _toList(); } + return _toList(); + } static FindResultState decode(Object result) { result as List; @@ -2509,7 +2445,9 @@ class FindResultState { if (identical(this, other)) { return true; } - return _deepEquals(activeMatchOrdinal, other.activeMatchOrdinal) && _deepEquals(numberOfMatches, other.numberOfMatches) && _deepEquals(isDoneCounting, other.isDoneCounting); + return _deepEquals(activeMatchOrdinal, other.activeMatchOrdinal) && + _deepEquals(numberOfMatches, other.numberOfMatches) && + _deepEquals(isDoneCounting, other.isDoneCounting); } @override @@ -2518,11 +2456,7 @@ class FindResultState { } class CustomSelectionAction { - CustomSelectionAction({ - required this.id, - required this.title, - this.pattern, - }); + CustomSelectionAction({required this.id, required this.title, this.pattern}); String id; @@ -2531,15 +2465,12 @@ class CustomSelectionAction { SelectionPattern? pattern; List _toList() { - return [ - id, - title, - pattern, - ]; + return [id, title, pattern]; } Object encode() { - return _toList(); } + return _toList(); + } static CustomSelectionAction decode(Object result) { result as List; @@ -2559,7 +2490,9 @@ class CustomSelectionAction { if (identical(this, other)) { return true; } - return _deepEquals(id, other.id) && _deepEquals(title, other.title) && _deepEquals(pattern, other.pattern); + return _deepEquals(id, other.id) && + _deepEquals(title, other.title) && + _deepEquals(pattern, other.pattern); } @override @@ -2601,7 +2534,8 @@ class WebExtensionData { } Object encode() { - return _toList(); } + return _toList(); + } static WebExtensionData decode(Object result) { result as List; @@ -2624,7 +2558,12 @@ class WebExtensionData { if (identical(this, other)) { return true; } - return _deepEquals(extensionId, other.extensionId) && _deepEquals(title, other.title) && _deepEquals(enabled, other.enabled) && _deepEquals(badgeText, other.badgeText) && _deepEquals(badgeTextColor, other.badgeTextColor) && _deepEquals(badgeBackgroundColor, other.badgeBackgroundColor); + return _deepEquals(extensionId, other.extensionId) && + _deepEquals(title, other.title) && + _deepEquals(enabled, other.enabled) && + _deepEquals(badgeText, other.badgeText) && + _deepEquals(badgeTextColor, other.badgeTextColor) && + _deepEquals(badgeBackgroundColor, other.badgeBackgroundColor); } @override @@ -2758,7 +2697,8 @@ class AddonInfo { } Object encode() { - return _toList(); } + return _toList(); + } static AddonInfo decode(Object result) { result as List; @@ -2771,7 +2711,8 @@ class AddonInfo { version: result[5]! as String, installedVersion: result[6] as String?, translatedPermissions: (result[7]! as List).cast(), - translatedRequiredDataCollectionPermissions: (result[8]! as List).cast(), + translatedRequiredDataCollectionPermissions: (result[8]! as List) + .cast(), authorName: result[9] as String?, authorUrl: result[10] as String?, homepageUrl: result[11]! as String, @@ -2804,7 +2745,41 @@ class AddonInfo { if (identical(this, other)) { return true; } - return _deepEquals(id, other.id) && _deepEquals(displayName, other.displayName) && _deepEquals(summary, other.summary) && _deepEquals(description, other.description) && _deepEquals(downloadUrl, other.downloadUrl) && _deepEquals(version, other.version) && _deepEquals(installedVersion, other.installedVersion) && _deepEquals(translatedPermissions, other.translatedPermissions) && _deepEquals(translatedRequiredDataCollectionPermissions, other.translatedRequiredDataCollectionPermissions) && _deepEquals(authorName, other.authorName) && _deepEquals(authorUrl, other.authorUrl) && _deepEquals(homepageUrl, other.homepageUrl) && _deepEquals(detailUrl, other.detailUrl) && _deepEquals(ratingUrl, other.ratingUrl) && _deepEquals(ratingAverage, other.ratingAverage) && _deepEquals(ratingReviews, other.ratingReviews) && _deepEquals(createdAt, other.createdAt) && _deepEquals(updatedAt, other.updatedAt) && _deepEquals(icon, other.icon) && _deepEquals(isInstalled, other.isInstalled) && _deepEquals(isEnabled, other.isEnabled) && _deepEquals(isSupported, other.isSupported) && _deepEquals(isAllowedInPrivateBrowsing, other.isAllowedInPrivateBrowsing) && _deepEquals(isAutoUpdateEnabled, other.isAutoUpdateEnabled) && _deepEquals(isLocalFileInstalled, other.isLocalFileInstalled) && _deepEquals(optionsPageUrl, other.optionsPageUrl) && _deepEquals(openOptionsPageInTab, other.openOptionsPageInTab) && _deepEquals(disabledReason, other.disabledReason) && _deepEquals(incognito, other.incognito); + return _deepEquals(id, other.id) && + _deepEquals(displayName, other.displayName) && + _deepEquals(summary, other.summary) && + _deepEquals(description, other.description) && + _deepEquals(downloadUrl, other.downloadUrl) && + _deepEquals(version, other.version) && + _deepEquals(installedVersion, other.installedVersion) && + _deepEquals(translatedPermissions, other.translatedPermissions) && + _deepEquals( + translatedRequiredDataCollectionPermissions, + other.translatedRequiredDataCollectionPermissions, + ) && + _deepEquals(authorName, other.authorName) && + _deepEquals(authorUrl, other.authorUrl) && + _deepEquals(homepageUrl, other.homepageUrl) && + _deepEquals(detailUrl, other.detailUrl) && + _deepEquals(ratingUrl, other.ratingUrl) && + _deepEquals(ratingAverage, other.ratingAverage) && + _deepEquals(ratingReviews, other.ratingReviews) && + _deepEquals(createdAt, other.createdAt) && + _deepEquals(updatedAt, other.updatedAt) && + _deepEquals(icon, other.icon) && + _deepEquals(isInstalled, other.isInstalled) && + _deepEquals(isEnabled, other.isEnabled) && + _deepEquals(isSupported, other.isSupported) && + _deepEquals( + isAllowedInPrivateBrowsing, + other.isAllowedInPrivateBrowsing, + ) && + _deepEquals(isAutoUpdateEnabled, other.isAutoUpdateEnabled) && + _deepEquals(isLocalFileInstalled, other.isLocalFileInstalled) && + _deepEquals(optionsPageUrl, other.optionsPageUrl) && + _deepEquals(openOptionsPageInTab, other.openOptionsPageInTab) && + _deepEquals(disabledReason, other.disabledReason) && + _deepEquals(incognito, other.incognito); } @override @@ -2826,15 +2801,12 @@ class AddonListingPreview { String? caption; List _toList() { - return [ - imageUrl, - thumbnailUrl, - caption, - ]; + return [imageUrl, thumbnailUrl, caption]; } Object encode() { - return _toList(); } + return _toList(); + } static AddonListingPreview decode(Object result) { result as List; @@ -2854,7 +2826,9 @@ class AddonListingPreview { if (identical(this, other)) { return true; } - return _deepEquals(imageUrl, other.imageUrl) && _deepEquals(thumbnailUrl, other.thumbnailUrl) && _deepEquals(caption, other.caption); + return _deepEquals(imageUrl, other.imageUrl) && + _deepEquals(thumbnailUrl, other.thumbnailUrl) && + _deepEquals(caption, other.caption); } @override @@ -2992,7 +2966,8 @@ class AddonListing { } Object encode() { - return _toList(); } + return _toList(); + } static AddonListing decode(Object result) { result as List; @@ -3039,7 +3014,39 @@ class AddonListing { if (identical(this, other)) { return true; } - return _deepEquals(id, other.id) && _deepEquals(name, other.name) && _deepEquals(summary, other.summary) && _deepEquals(description, other.description) && _deepEquals(iconUrl, other.iconUrl) && _deepEquals(latestVersion, other.latestVersion) && _deepEquals(downloadUrl, other.downloadUrl) && _deepEquals(ratingAverage, other.ratingAverage) && _deepEquals(ratingReviews, other.ratingReviews) && _deepEquals(authorName, other.authorName) && _deepEquals(authorUrl, other.authorUrl) && _deepEquals(homepageUrl, other.homepageUrl) && _deepEquals(detailUrl, other.detailUrl) && _deepEquals(ratingUrl, other.ratingUrl) && _deepEquals(averageDailyUsers, other.averageDailyUsers) && _deepEquals(promoted, other.promoted) && _deepEquals(previews, other.previews) && _deepEquals(permissions, other.permissions) && _deepEquals(hostPermissions, other.hostPermissions) && _deepEquals(optionalPermissions, other.optionalPermissions) && _deepEquals(dataCollectionPermissions, other.dataCollectionPermissions) && _deepEquals(fileSize, other.fileSize) && _deepEquals(lastUpdated, other.lastUpdated) && _deepEquals(licenseName, other.licenseName) && _deepEquals(licenseUrl, other.licenseUrl) && _deepEquals(supportUrl, other.supportUrl) && _deepEquals(supportEmail, other.supportEmail) && _deepEquals(categories, other.categories) && _deepEquals(hasPrivacyPolicy, other.hasPrivacyPolicy) && _deepEquals(slug, other.slug); + return _deepEquals(id, other.id) && + _deepEquals(name, other.name) && + _deepEquals(summary, other.summary) && + _deepEquals(description, other.description) && + _deepEquals(iconUrl, other.iconUrl) && + _deepEquals(latestVersion, other.latestVersion) && + _deepEquals(downloadUrl, other.downloadUrl) && + _deepEquals(ratingAverage, other.ratingAverage) && + _deepEquals(ratingReviews, other.ratingReviews) && + _deepEquals(authorName, other.authorName) && + _deepEquals(authorUrl, other.authorUrl) && + _deepEquals(homepageUrl, other.homepageUrl) && + _deepEquals(detailUrl, other.detailUrl) && + _deepEquals(ratingUrl, other.ratingUrl) && + _deepEquals(averageDailyUsers, other.averageDailyUsers) && + _deepEquals(promoted, other.promoted) && + _deepEquals(previews, other.previews) && + _deepEquals(permissions, other.permissions) && + _deepEquals(hostPermissions, other.hostPermissions) && + _deepEquals(optionalPermissions, other.optionalPermissions) && + _deepEquals( + dataCollectionPermissions, + other.dataCollectionPermissions, + ) && + _deepEquals(fileSize, other.fileSize) && + _deepEquals(lastUpdated, other.lastUpdated) && + _deepEquals(licenseName, other.licenseName) && + _deepEquals(licenseUrl, other.licenseUrl) && + _deepEquals(supportUrl, other.supportUrl) && + _deepEquals(supportEmail, other.supportEmail) && + _deepEquals(categories, other.categories) && + _deepEquals(hasPrivacyPolicy, other.hasPrivacyPolicy) && + _deepEquals(slug, other.slug); } @override @@ -3101,7 +3108,8 @@ class AddonStoreInfo { } Object encode() { - return _toList(); } + return _toList(); + } static AddonStoreInfo decode(Object result) { result as List; @@ -3129,7 +3137,17 @@ class AddonStoreInfo { if (identical(this, other)) { return true; } - return _deepEquals(latestVersion, other.latestVersion) && _deepEquals(latestXpiUrl, other.latestXpiUrl) && _deepEquals(ratingAverage, other.ratingAverage) && _deepEquals(ratingReviews, other.ratingReviews) && _deepEquals(summary, other.summary) && _deepEquals(description, other.description) && _deepEquals(homepageUrl, other.homepageUrl) && _deepEquals(detailUrl, other.detailUrl) && _deepEquals(ratingUrl, other.ratingUrl) && _deepEquals(authorName, other.authorName) && _deepEquals(authorUrl, other.authorUrl); + return _deepEquals(latestVersion, other.latestVersion) && + _deepEquals(latestXpiUrl, other.latestXpiUrl) && + _deepEquals(ratingAverage, other.ratingAverage) && + _deepEquals(ratingReviews, other.ratingReviews) && + _deepEquals(summary, other.summary) && + _deepEquals(description, other.description) && + _deepEquals(homepageUrl, other.homepageUrl) && + _deepEquals(detailUrl, other.detailUrl) && + _deepEquals(ratingUrl, other.ratingUrl) && + _deepEquals(authorName, other.authorName) && + _deepEquals(authorUrl, other.authorUrl); } @override @@ -3154,16 +3172,12 @@ class AddonUpdateAttemptInfo { String? message; List _toList() { - return [ - addonId, - dateMillisecondsSinceEpoch, - status, - message, - ]; + return [addonId, dateMillisecondsSinceEpoch, status, message]; } Object encode() { - return _toList(); } + return _toList(); + } static AddonUpdateAttemptInfo decode(Object result) { result as List; @@ -3184,7 +3198,13 @@ class AddonUpdateAttemptInfo { if (identical(this, other)) { return true; } - return _deepEquals(addonId, other.addonId) && _deepEquals(dateMillisecondsSinceEpoch, other.dateMillisecondsSinceEpoch) && _deepEquals(status, other.status) && _deepEquals(message, other.message); + return _deepEquals(addonId, other.addonId) && + _deepEquals( + dateMillisecondsSinceEpoch, + other.dateMillisecondsSinceEpoch, + ) && + _deepEquals(status, other.status) && + _deepEquals(message, other.message); } @override @@ -3218,19 +3238,12 @@ class GeckoSuggestion { Uint8List? icon; List _toList() { - return [ - id, - type, - score, - title, - description, - editSuggestion, - icon, - ]; + return [id, type, score, title, description, editSuggestion, icon]; } Object encode() { - return _toList(); } + return _toList(); + } static GeckoSuggestion decode(Object result) { result as List; @@ -3254,7 +3267,13 @@ class GeckoSuggestion { if (identical(this, other)) { return true; } - return _deepEquals(id, other.id) && _deepEquals(type, other.type) && _deepEquals(score, other.score) && _deepEquals(title, other.title) && _deepEquals(description, other.description) && _deepEquals(editSuggestion, other.editSuggestion) && _deepEquals(icon, other.icon); + return _deepEquals(id, other.id) && + _deepEquals(type, other.type) && + _deepEquals(score, other.score) && + _deepEquals(title, other.title) && + _deepEquals(description, other.description) && + _deepEquals(editSuggestion, other.editSuggestion) && + _deepEquals(icon, other.icon); } @override @@ -3296,7 +3315,8 @@ class TabContent { } Object encode() { - return _toList(); } + return _toList(); + } static TabContent decode(Object result) { result as List; @@ -3319,7 +3339,12 @@ class TabContent { if (identical(this, other)) { return true; } - return _deepEquals(tabId, other.tabId) && _deepEquals(fullContentMarkdown, other.fullContentMarkdown) && _deepEquals(fullContentPlain, other.fullContentPlain) && _deepEquals(isProbablyReaderable, other.isProbablyReaderable) && _deepEquals(extractedContentMarkdown, other.extractedContentMarkdown) && _deepEquals(extractedContentPlain, other.extractedContentPlain); + return _deepEquals(tabId, other.tabId) && + _deepEquals(fullContentMarkdown, other.fullContentMarkdown) && + _deepEquals(fullContentPlain, other.fullContentPlain) && + _deepEquals(isProbablyReaderable, other.isProbablyReaderable) && + _deepEquals(extractedContentMarkdown, other.extractedContentMarkdown) && + _deepEquals(extractedContentPlain, other.extractedContentPlain); } @override @@ -3353,7 +3378,8 @@ class ContentBlocking { } Object encode() { - return _toList(); } + return _toList(); + } static ContentBlocking decode(Object result) { result as List; @@ -3374,7 +3400,22 @@ class ContentBlocking { if (identical(this, other)) { return true; } - return _deepEquals(queryParameterStripping, other.queryParameterStripping) && _deepEquals(queryParameterStrippingAllowList, other.queryParameterStrippingAllowList) && _deepEquals(queryParameterStrippingStripList, other.queryParameterStrippingStripList) && _deepEquals(bounceTrackingProtectionMode, other.bounceTrackingProtectionMode); + return _deepEquals( + queryParameterStripping, + other.queryParameterStripping, + ) && + _deepEquals( + queryParameterStrippingAllowList, + other.queryParameterStrippingAllowList, + ) && + _deepEquals( + queryParameterStrippingStripList, + other.queryParameterStrippingStripList, + ) && + _deepEquals( + bounceTrackingProtectionMode, + other.bounceTrackingProtectionMode, + ); } @override @@ -3408,7 +3449,8 @@ class DohSettings { } Object encode() { - return _toList(); } + return _toList(); + } static DohSettings decode(Object result) { result as List; @@ -3429,7 +3471,10 @@ class DohSettings { if (identical(this, other)) { return true; } - return _deepEquals(dohSettingsMode, other.dohSettingsMode) && _deepEquals(dohProviderUrl, other.dohProviderUrl) && _deepEquals(dohDefaultProviderUrl, other.dohDefaultProviderUrl) && _deepEquals(dohExceptionsList, other.dohExceptionsList); + return _deepEquals(dohSettingsMode, other.dohSettingsMode) && + _deepEquals(dohProviderUrl, other.dohProviderUrl) && + _deepEquals(dohDefaultProviderUrl, other.dohDefaultProviderUrl) && + _deepEquals(dohExceptionsList, other.dohExceptionsList); } @override @@ -3627,7 +3672,8 @@ class GeckoEngineSettings { } Object encode() { - return _toList(); } + return _toList(); + } static GeckoEngineSettings decode(Object result) { result as List; @@ -3638,7 +3684,8 @@ class GeckoEngineSettings { globalPrivacyControlEnabled: result[3] as bool?, preferredColorScheme: result[4] as ColorScheme?, cookieBannerHandlingMode: result[5] as CookieBannerHandlingMode?, - cookieBannerHandlingModePrivateBrowsing: result[6] as CookieBannerHandlingMode?, + cookieBannerHandlingModePrivateBrowsing: + result[6] as CookieBannerHandlingMode?, cookieBannerHandlingGlobalRules: result[7] as bool?, cookieBannerHandlingGlobalRulesSubFrames: result[8] as bool?, webContentIsolationStrategy: result[9] as WebContentIsolationStrategy?, @@ -3686,7 +3733,75 @@ class GeckoEngineSettings { if (identical(this, other)) { return true; } - return _deepEquals(javascriptEnabled, other.javascriptEnabled) && _deepEquals(trackingProtectionPolicy, other.trackingProtectionPolicy) && _deepEquals(httpsOnlyMode, other.httpsOnlyMode) && _deepEquals(globalPrivacyControlEnabled, other.globalPrivacyControlEnabled) && _deepEquals(preferredColorScheme, other.preferredColorScheme) && _deepEquals(cookieBannerHandlingMode, other.cookieBannerHandlingMode) && _deepEquals(cookieBannerHandlingModePrivateBrowsing, other.cookieBannerHandlingModePrivateBrowsing) && _deepEquals(cookieBannerHandlingGlobalRules, other.cookieBannerHandlingGlobalRules) && _deepEquals(cookieBannerHandlingGlobalRulesSubFrames, other.cookieBannerHandlingGlobalRulesSubFrames) && _deepEquals(webContentIsolationStrategy, other.webContentIsolationStrategy) && _deepEquals(userAgent, other.userAgent) && _deepEquals(contentBlocking, other.contentBlocking) && _deepEquals(enterpriseRootsEnabled, other.enterpriseRootsEnabled) && _deepEquals(dohSettings, other.dohSettings) && _deepEquals(fingerprintingProtectionOverrides, other.fingerprintingProtectionOverrides) && _deepEquals(locales, other.locales) && _deepEquals(blockCookies, other.blockCookies) && _deepEquals(customCookiePolicy, other.customCookiePolicy) && _deepEquals(blockTrackingContent, other.blockTrackingContent) && _deepEquals(trackingContentScope, other.trackingContentScope) && _deepEquals(blockCryptominers, other.blockCryptominers) && _deepEquals(blockFingerprinters, other.blockFingerprinters) && _deepEquals(blockRedirectTrackers, other.blockRedirectTrackers) && _deepEquals(blockSuspectedFingerprinters, other.blockSuspectedFingerprinters) && _deepEquals(suspectedFingerprintersScope, other.suspectedFingerprintersScope) && _deepEquals(allowListBaseline, other.allowListBaseline) && _deepEquals(allowListConvenience, other.allowListConvenience) && _deepEquals(webFontsEnabled, other.webFontsEnabled) && _deepEquals(automaticFontSizeAdjustment, other.automaticFontSizeAdjustment) && _deepEquals(fontSizeFactor, other.fontSizeFactor) && _deepEquals(fontInflationEnabled, other.fontInflationEnabled) && _deepEquals(displayDensityOverride, other.displayDensityOverride) && _deepEquals(screenWidthOverride, other.screenWidthOverride) && _deepEquals(screenHeightOverride, other.screenHeightOverride) && _deepEquals(inputAutoZoomEnabled, other.inputAutoZoomEnabled) && _deepEquals(fissionEnabled, other.fissionEnabled) && _deepEquals(isolatedProcessEnabled, other.isolatedProcessEnabled) && _deepEquals(appZygoteProcessEnabled, other.appZygoteProcessEnabled) && _deepEquals(extensionsWebAPIEnabled, other.extensionsWebAPIEnabled) && _deepEquals(lnaBlocking, other.lnaBlocking) && _deepEquals(lnaBlockTrackers, other.lnaBlockTrackers) && _deepEquals(lnaEnabled, other.lnaEnabled); + return _deepEquals(javascriptEnabled, other.javascriptEnabled) && + _deepEquals(trackingProtectionPolicy, other.trackingProtectionPolicy) && + _deepEquals(httpsOnlyMode, other.httpsOnlyMode) && + _deepEquals( + globalPrivacyControlEnabled, + other.globalPrivacyControlEnabled, + ) && + _deepEquals(preferredColorScheme, other.preferredColorScheme) && + _deepEquals(cookieBannerHandlingMode, other.cookieBannerHandlingMode) && + _deepEquals( + cookieBannerHandlingModePrivateBrowsing, + other.cookieBannerHandlingModePrivateBrowsing, + ) && + _deepEquals( + cookieBannerHandlingGlobalRules, + other.cookieBannerHandlingGlobalRules, + ) && + _deepEquals( + cookieBannerHandlingGlobalRulesSubFrames, + other.cookieBannerHandlingGlobalRulesSubFrames, + ) && + _deepEquals( + webContentIsolationStrategy, + other.webContentIsolationStrategy, + ) && + _deepEquals(userAgent, other.userAgent) && + _deepEquals(contentBlocking, other.contentBlocking) && + _deepEquals(enterpriseRootsEnabled, other.enterpriseRootsEnabled) && + _deepEquals(dohSettings, other.dohSettings) && + _deepEquals( + fingerprintingProtectionOverrides, + other.fingerprintingProtectionOverrides, + ) && + _deepEquals(locales, other.locales) && + _deepEquals(blockCookies, other.blockCookies) && + _deepEquals(customCookiePolicy, other.customCookiePolicy) && + _deepEquals(blockTrackingContent, other.blockTrackingContent) && + _deepEquals(trackingContentScope, other.trackingContentScope) && + _deepEquals(blockCryptominers, other.blockCryptominers) && + _deepEquals(blockFingerprinters, other.blockFingerprinters) && + _deepEquals(blockRedirectTrackers, other.blockRedirectTrackers) && + _deepEquals( + blockSuspectedFingerprinters, + other.blockSuspectedFingerprinters, + ) && + _deepEquals( + suspectedFingerprintersScope, + other.suspectedFingerprintersScope, + ) && + _deepEquals(allowListBaseline, other.allowListBaseline) && + _deepEquals(allowListConvenience, other.allowListConvenience) && + _deepEquals(webFontsEnabled, other.webFontsEnabled) && + _deepEquals( + automaticFontSizeAdjustment, + other.automaticFontSizeAdjustment, + ) && + _deepEquals(fontSizeFactor, other.fontSizeFactor) && + _deepEquals(fontInflationEnabled, other.fontInflationEnabled) && + _deepEquals(displayDensityOverride, other.displayDensityOverride) && + _deepEquals(screenWidthOverride, other.screenWidthOverride) && + _deepEquals(screenHeightOverride, other.screenHeightOverride) && + _deepEquals(inputAutoZoomEnabled, other.inputAutoZoomEnabled) && + _deepEquals(fissionEnabled, other.fissionEnabled) && + _deepEquals(isolatedProcessEnabled, other.isolatedProcessEnabled) && + _deepEquals(appZygoteProcessEnabled, other.appZygoteProcessEnabled) && + _deepEquals(extensionsWebAPIEnabled, other.extensionsWebAPIEnabled) && + _deepEquals(lnaBlocking, other.lnaBlocking) && + _deepEquals(lnaBlockTrackers, other.lnaBlockTrackers) && + _deepEquals(lnaEnabled, other.lnaEnabled); } @override @@ -3714,17 +3829,12 @@ class AutocompleteResult { int totalItems; List _toList() { - return [ - input, - text, - url, - source, - totalItems, - ]; + return [input, text, url, source, totalItems]; } Object encode() { - return _toList(); } + return _toList(); + } static AutocompleteResult decode(Object result) { result as List; @@ -3746,7 +3856,11 @@ class AutocompleteResult { if (identical(this, other)) { return true; } - return _deepEquals(input, other.input) && _deepEquals(text, other.text) && _deepEquals(url, other.url) && _deepEquals(source, other.source) && _deepEquals(totalItems, other.totalItems); + return _deepEquals(input, other.input) && + _deepEquals(text, other.text) && + _deepEquals(url, other.url) && + _deepEquals(source, other.source) && + _deepEquals(totalItems, other.totalItems); } @override @@ -3756,29 +3870,23 @@ class AutocompleteResult { /// Represents all the different supported types of data that can be found from long clicking /// an element. -sealed class HitResult { -} +sealed class HitResult {} /// Default type if we're unable to match the type to anything. It may or may not have a src. class UnknownHitResult extends HitResult { - UnknownHitResult({ - required this.src, - this.linkText, - }); + UnknownHitResult({required this.src, this.linkText}); String src; String? linkText; List _toList() { - return [ - src, - linkText, - ]; + return [src, linkText]; } Object encode() { - return _toList(); } + return _toList(); + } static UnknownHitResult decode(Object result) { result as List; @@ -3807,24 +3915,19 @@ class UnknownHitResult extends HitResult { /// If the HTML element was of type 'HTMLImageElement'. class ImageHitResult extends HitResult { - ImageHitResult({ - required this.src, - this.title, - }); + ImageHitResult({required this.src, this.title}); String src; String? title; List _toList() { - return [ - src, - title, - ]; + return [src, title]; } Object encode() { - return _toList(); } + return _toList(); + } static ImageHitResult decode(Object result) { result as List; @@ -3853,24 +3956,19 @@ class ImageHitResult extends HitResult { /// If the HTML element was of type 'HTMLVideoElement'. class VideoHitResult extends HitResult { - VideoHitResult({ - required this.src, - this.title, - }); + VideoHitResult({required this.src, this.title}); String src; String? title; List _toList() { - return [ - src, - title, - ]; + return [src, title]; } Object encode() { - return _toList(); } + return _toList(); + } static VideoHitResult decode(Object result) { result as List; @@ -3899,24 +3997,19 @@ class VideoHitResult extends HitResult { /// If the HTML element was of type 'HTMLAudioElement'. class AudioHitResult extends HitResult { - AudioHitResult({ - required this.src, - this.title, - }); + AudioHitResult({required this.src, this.title}); String src; String? title; List _toList() { - return [ - src, - title, - ]; + return [src, title]; } Object encode() { - return _toList(); } + return _toList(); + } static AudioHitResult decode(Object result) { result as List; @@ -3945,24 +4038,19 @@ class AudioHitResult extends HitResult { /// If the HTML element was of type 'HTMLImageElement' and contained a URI. class ImageSrcHitResult extends HitResult { - ImageSrcHitResult({ - required this.src, - required this.uri, - }); + ImageSrcHitResult({required this.src, required this.uri}); String src; String uri; List _toList() { - return [ - src, - uri, - ]; + return [src, uri]; } Object encode() { - return _toList(); } + return _toList(); + } static ImageSrcHitResult decode(Object result) { result as List; @@ -3991,26 +4079,21 @@ class ImageSrcHitResult extends HitResult { /// The type used if the URI is prepended with 'tel:'. class PhoneHitResult extends HitResult { - PhoneHitResult({ - required this.src, - }); + PhoneHitResult({required this.src}); String src; List _toList() { - return [ - src, - ]; + return [src]; } Object encode() { - return _toList(); } + return _toList(); + } static PhoneHitResult decode(Object result) { result as List; - return PhoneHitResult( - src: result[0]! as String, - ); + return PhoneHitResult(src: result[0]! as String); } @override @@ -4032,26 +4115,21 @@ class PhoneHitResult extends HitResult { /// The type used if the URI is prepended with 'mailto:'. class EmailHitResult extends HitResult { - EmailHitResult({ - required this.src, - }); + EmailHitResult({required this.src}); String src; List _toList() { - return [ - src, - ]; + return [src]; } Object encode() { - return _toList(); } + return _toList(); + } static EmailHitResult decode(Object result) { result as List; - return EmailHitResult( - src: result[0]! as String, - ); + return EmailHitResult(src: result[0]! as String); } @override @@ -4073,26 +4151,21 @@ class EmailHitResult extends HitResult { /// The type used if the URI is prepended with 'geo:'. class GeoHitResult extends HitResult { - GeoHitResult({ - required this.src, - }); + GeoHitResult({required this.src}); String src; List _toList() { - return [ - src, - ]; + return [src]; } Object encode() { - return _toList(); } + return _toList(); + } static GeoHitResult decode(Object result) { result as List; - return GeoHitResult( - src: result[0]! as String, - ); + return GeoHitResult(src: result[0]! as String); } @override @@ -4190,7 +4263,8 @@ class DownloadState { } Object encode() { - return _toList(); } + return _toList(); + } static DownloadState decode(Object result) { result as List; @@ -4224,7 +4298,23 @@ class DownloadState { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(fileName, other.fileName) && _deepEquals(contentType, other.contentType) && _deepEquals(contentLength, other.contentLength) && _deepEquals(currentBytesCopied, other.currentBytesCopied) && _deepEquals(status, other.status) && _deepEquals(userAgent, other.userAgent) && _deepEquals(destinationDirectory, other.destinationDirectory) && _deepEquals(directoryPath, other.directoryPath) && _deepEquals(referrerUrl, other.referrerUrl) && _deepEquals(skipConfirmation, other.skipConfirmation) && _deepEquals(openInApp, other.openInApp) && _deepEquals(id, other.id) && _deepEquals(sessionId, other.sessionId) && _deepEquals(private, other.private) && _deepEquals(createdTime, other.createdTime) && _deepEquals(notificationId, other.notificationId); + return _deepEquals(url, other.url) && + _deepEquals(fileName, other.fileName) && + _deepEquals(contentType, other.contentType) && + _deepEquals(contentLength, other.contentLength) && + _deepEquals(currentBytesCopied, other.currentBytesCopied) && + _deepEquals(status, other.status) && + _deepEquals(userAgent, other.userAgent) && + _deepEquals(destinationDirectory, other.destinationDirectory) && + _deepEquals(directoryPath, other.directoryPath) && + _deepEquals(referrerUrl, other.referrerUrl) && + _deepEquals(skipConfirmation, other.skipConfirmation) && + _deepEquals(openInApp, other.openInApp) && + _deepEquals(id, other.id) && + _deepEquals(sessionId, other.sessionId) && + _deepEquals(private, other.private) && + _deepEquals(createdTime, other.createdTime) && + _deepEquals(notificationId, other.notificationId); } @override @@ -4249,16 +4339,12 @@ class ShareInternetResourceState { String? referrerUrl; List _toList() { - return [ - url, - contentType, - private, - referrerUrl, - ]; + return [url, contentType, private, referrerUrl]; } Object encode() { - return _toList(); } + return _toList(); + } static ShareInternetResourceState decode(Object result) { result as List; @@ -4273,13 +4359,17 @@ class ShareInternetResourceState { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! ShareInternetResourceState || other.runtimeType != runtimeType) { + if (other is! ShareInternetResourceState || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(contentType, other.contentType) && _deepEquals(private, other.private) && _deepEquals(referrerUrl, other.referrerUrl); + return _deepEquals(url, other.url) && + _deepEquals(contentType, other.contentType) && + _deepEquals(private, other.private) && + _deepEquals(referrerUrl, other.referrerUrl); } @override @@ -4301,15 +4391,12 @@ class AddonCollection { String collectionName; List _toList() { - return [ - serverURL, - collectionUser, - collectionName, - ]; + return [serverURL, collectionUser, collectionName]; } Object encode() { - return _toList(); } + return _toList(); + } static AddonCollection decode(Object result) { result as List; @@ -4329,7 +4416,9 @@ class AddonCollection { if (identical(this, other)) { return true; } - return _deepEquals(serverURL, other.serverURL) && _deepEquals(collectionUser, other.collectionUser) && _deepEquals(collectionName, other.collectionName); + return _deepEquals(serverURL, other.serverURL) && + _deepEquals(collectionUser, other.collectionUser) && + _deepEquals(collectionName, other.collectionName); } @override @@ -4338,24 +4427,19 @@ class AddonCollection { } class SyncEngineStatus { - SyncEngineStatus({ - required this.engine, - required this.enabled, - }); + SyncEngineStatus({required this.engine, required this.enabled}); SyncEngineValue engine; bool enabled; List _toList() { - return [ - engine, - enabled, - ]; + return [engine, enabled]; } Object encode() { - return _toList(); } + return _toList(); + } static SyncEngineStatus decode(Object result) { result as List; @@ -4374,7 +4458,8 @@ class SyncEngineStatus { if (identical(this, other)) { return true; } - return _deepEquals(engine, other.engine) && _deepEquals(enabled, other.enabled); + return _deepEquals(engine, other.engine) && + _deepEquals(enabled, other.enabled); } @override @@ -4420,7 +4505,8 @@ class SyncAccountInfo { } Object encode() { - return _toList(); } + return _toList(); + } static SyncAccountInfo decode(Object result) { result as List; @@ -4444,7 +4530,13 @@ class SyncAccountInfo { if (identical(this, other)) { return true; } - return _deepEquals(authenticated, other.authenticated) && _deepEquals(syncing, other.syncing) && _deepEquals(needsReauth, other.needsReauth) && _deepEquals(email, other.email) && _deepEquals(displayName, other.displayName) && _deepEquals(lastSyncedAt, other.lastSyncedAt) && _deepEquals(engines, other.engines); + return _deepEquals(authenticated, other.authenticated) && + _deepEquals(syncing, other.syncing) && + _deepEquals(needsReauth, other.needsReauth) && + _deepEquals(email, other.email) && + _deepEquals(displayName, other.displayName) && + _deepEquals(lastSyncedAt, other.lastSyncedAt) && + _deepEquals(engines, other.engines); } @override @@ -4469,16 +4561,12 @@ class SyncDevice { bool canSendTab; List _toList() { - return [ - deviceId, - displayName, - isCurrentDevice, - canSendTab, - ]; + return [deviceId, displayName, isCurrentDevice, canSendTab]; } Object encode() { - return _toList(); } + return _toList(); + } static SyncDevice decode(Object result) { result as List; @@ -4499,7 +4587,10 @@ class SyncDevice { if (identical(this, other)) { return true; } - return _deepEquals(deviceId, other.deviceId) && _deepEquals(displayName, other.displayName) && _deepEquals(isCurrentDevice, other.isCurrentDevice) && _deepEquals(canSendTab, other.canSendTab); + return _deepEquals(deviceId, other.deviceId) && + _deepEquals(displayName, other.displayName) && + _deepEquals(isCurrentDevice, other.isCurrentDevice) && + _deepEquals(canSendTab, other.canSendTab); } @override @@ -4524,16 +4615,12 @@ class SyncIncomingTab { String? fromDeviceName; List _toList() { - return [ - title, - url, - fromDeviceId, - fromDeviceName, - ]; + return [title, url, fromDeviceId, fromDeviceName]; } Object encode() { - return _toList(); } + return _toList(); + } static SyncIncomingTab decode(Object result) { result as List; @@ -4554,7 +4641,10 @@ class SyncIncomingTab { if (identical(this, other)) { return true; } - return _deepEquals(title, other.title) && _deepEquals(url, other.url) && _deepEquals(fromDeviceId, other.fromDeviceId) && _deepEquals(fromDeviceName, other.fromDeviceName); + return _deepEquals(title, other.title) && + _deepEquals(url, other.url) && + _deepEquals(fromDeviceId, other.fromDeviceId) && + _deepEquals(fromDeviceName, other.fromDeviceName); } @override @@ -4582,17 +4672,12 @@ class SyncRemoteTab { bool inactive; List _toList() { - return [ - title, - url, - iconUrl, - lastUsed, - inactive, - ]; + return [title, url, iconUrl, lastUsed, inactive]; } Object encode() { - return _toList(); } + return _toList(); + } static SyncRemoteTab decode(Object result) { result as List; @@ -4614,7 +4699,11 @@ class SyncRemoteTab { if (identical(this, other)) { return true; } - return _deepEquals(title, other.title) && _deepEquals(url, other.url) && _deepEquals(iconUrl, other.iconUrl) && _deepEquals(lastUsed, other.lastUsed) && _deepEquals(inactive, other.inactive); + return _deepEquals(title, other.title) && + _deepEquals(url, other.url) && + _deepEquals(iconUrl, other.iconUrl) && + _deepEquals(lastUsed, other.lastUsed) && + _deepEquals(inactive, other.inactive); } @override @@ -4636,15 +4725,12 @@ class SyncDeviceTabs { List tabs; List _toList() { - return [ - deviceId, - deviceName, - tabs, - ]; + return [deviceId, deviceName, tabs]; } Object encode() { - return _toList(); } + return _toList(); + } static SyncDeviceTabs decode(Object result) { result as List; @@ -4664,7 +4750,9 @@ class SyncDeviceTabs { if (identical(this, other)) { return true; } - return _deepEquals(deviceId, other.deviceId) && _deepEquals(deviceName, other.deviceName) && _deepEquals(tabs, other.tabs); + return _deepEquals(deviceId, other.deviceId) && + _deepEquals(deviceName, other.deviceName) && + _deepEquals(tabs, other.tabs); } @override @@ -4692,17 +4780,12 @@ class GeckoPref { bool hasUserChangedValue; List _toList() { - return [ - name, - value, - defaultValue, - userValue, - hasUserChangedValue, - ]; + return [name, value, defaultValue, userValue, hasUserChangedValue]; } Object encode() { - return _toList(); } + return _toList(); + } static GeckoPref decode(Object result) { result as List; @@ -4724,7 +4807,11 @@ class GeckoPref { if (identical(this, other)) { return true; } - return _deepEquals(name, other.name) && _deepEquals(value, other.value) && _deepEquals(defaultValue, other.defaultValue) && _deepEquals(userValue, other.userValue) && _deepEquals(hasUserChangedValue, other.hasUserChangedValue); + return _deepEquals(name, other.name) && + _deepEquals(value, other.value) && + _deepEquals(defaultValue, other.defaultValue) && + _deepEquals(userValue, other.userValue) && + _deepEquals(hasUserChangedValue, other.hasUserChangedValue); } @override @@ -4793,7 +4880,8 @@ class MlProgressData { } Object encode() { - return _toList(); } + return _toList(); + } static MlProgressData decode(Object result) { result as List; @@ -4820,7 +4908,105 @@ class MlProgressData { if (identical(this, other)) { return true; } - return _deepEquals(modelType, other.modelType) && _deepEquals(progress, other.progress) && _deepEquals(type, other.type) && _deepEquals(status, other.status) && _deepEquals(totalLoaded, other.totalLoaded) && _deepEquals(currentLoaded, other.currentLoaded) && _deepEquals(total, other.total) && _deepEquals(units, other.units) && _deepEquals(ok, other.ok) && _deepEquals(id, other.id); + return _deepEquals(modelType, other.modelType) && + _deepEquals(progress, other.progress) && + _deepEquals(type, other.type) && + _deepEquals(status, other.status) && + _deepEquals(totalLoaded, other.totalLoaded) && + _deepEquals(currentLoaded, other.currentLoaded) && + _deepEquals(total, other.total) && + _deepEquals(units, other.units) && + _deepEquals(ok, other.ok) && + _deepEquals(id, other.id); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class GeckoProxySettings { + GeckoProxySettings({ + required this.id, + required this.title, + required this.type, + required this.host, + required this.port, + this.username, + this.password, + required this.proxyDNS, + required this.doNotProxyLocal, + }); + + String id; + + String title; + + String type; + + String host; + + int port; + + String? username; + + String? password; + + bool proxyDNS; + + bool doNotProxyLocal; + + List _toList() { + return [ + id, + title, + type, + host, + port, + username, + password, + proxyDNS, + doNotProxyLocal, + ]; + } + + Object encode() { + return _toList(); + } + + static GeckoProxySettings decode(Object result) { + result as List; + return GeckoProxySettings( + id: result[0]! as String, + title: result[1]! as String, + type: result[2]! as String, + host: result[3]! as String, + port: result[4]! as int, + username: result[5] as String?, + password: result[6] as String?, + proxyDNS: result[7]! as bool, + doNotProxyLocal: result[8]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! GeckoProxySettings || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(id, other.id) && + _deepEquals(title, other.title) && + _deepEquals(type, other.type) && + _deepEquals(host, other.host) && + _deepEquals(port, other.port) && + _deepEquals(username, other.username) && + _deepEquals(password, other.password) && + _deepEquals(proxyDNS, other.proxyDNS) && + _deepEquals(doNotProxyLocal, other.doNotProxyLocal); } @override @@ -4848,17 +5034,12 @@ class ContainerSiteAssignment { bool blocked; List _toList() { - return [ - requestId, - tabId, - originUrl, - url, - blocked, - ]; + return [requestId, tabId, originUrl, url, blocked]; } Object encode() { - return _toList(); } + return _toList(); + } static ContainerSiteAssignment decode(Object result) { result as List; @@ -4880,7 +5061,65 @@ class ContainerSiteAssignment { if (identical(this, other)) { return true; } - return _deepEquals(requestId, other.requestId) && _deepEquals(tabId, other.tabId) && _deepEquals(originUrl, other.originUrl) && _deepEquals(url, other.url) && _deepEquals(blocked, other.blocked); + return _deepEquals(requestId, other.requestId) && + _deepEquals(tabId, other.tabId) && + _deepEquals(originUrl, other.originUrl) && + _deepEquals(url, other.url) && + _deepEquals(blocked, other.blocked); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class ProxyLoadError { + ProxyLoadError({ + this.tabId, + this.contextId, + this.url, + required this.errorType, + }); + + String? tabId; + + String? contextId; + + String? url; + + String errorType; + + List _toList() { + return [tabId, contextId, url, errorType]; + } + + Object encode() { + return _toList(); + } + + static ProxyLoadError decode(Object result) { + result as List; + return ProxyLoadError( + tabId: result[0] as String?, + contextId: result[1] as String?, + url: result[2] as String?, + errorType: result[3]! as String, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! ProxyLoadError || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(tabId, other.tabId) && + _deepEquals(contextId, other.contextId) && + _deepEquals(url, other.url) && + _deepEquals(errorType, other.errorType); } @override @@ -4889,31 +5128,23 @@ class ContainerSiteAssignment { } class GeckoHeader { - GeckoHeader({ - required this.key, - required this.value, - }); + GeckoHeader({required this.key, required this.value}); String key; String value; List _toList() { - return [ - key, - value, - ]; + return [key, value]; } Object encode() { - return _toList(); } + return _toList(); + } static GeckoHeader decode(Object result) { result as List; - return GeckoHeader( - key: result[0]! as String, - value: result[1]! as String, - ); + return GeckoHeader(key: result[0]! as String, value: result[1]! as String); } @override @@ -4995,7 +5226,8 @@ class GeckoFetchRequest { } Object encode() { - return _toList(); } + return _toList(); + } static GeckoFetchRequest decode(Object result) { result as List; @@ -5025,7 +5257,19 @@ class GeckoFetchRequest { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(method, other.method) && _deepEquals(headers, other.headers) && _deepEquals(connectTimeoutMillis, other.connectTimeoutMillis) && _deepEquals(readTimeoutMillis, other.readTimeoutMillis) && _deepEquals(body, other.body) && _deepEquals(redirect, other.redirect) && _deepEquals(cookiePolicy, other.cookiePolicy) && _deepEquals(useCaches, other.useCaches) && _deepEquals(private, other.private) && _deepEquals(useOhttp, other.useOhttp) && _deepEquals(referrerUrl, other.referrerUrl) && _deepEquals(conservative, other.conservative); + return _deepEquals(url, other.url) && + _deepEquals(method, other.method) && + _deepEquals(headers, other.headers) && + _deepEquals(connectTimeoutMillis, other.connectTimeoutMillis) && + _deepEquals(readTimeoutMillis, other.readTimeoutMillis) && + _deepEquals(body, other.body) && + _deepEquals(redirect, other.redirect) && + _deepEquals(cookiePolicy, other.cookiePolicy) && + _deepEquals(useCaches, other.useCaches) && + _deepEquals(private, other.private) && + _deepEquals(useOhttp, other.useOhttp) && + _deepEquals(referrerUrl, other.referrerUrl) && + _deepEquals(conservative, other.conservative); } @override @@ -5050,16 +5294,12 @@ class GeckoFetchResponse { Uint8List body; List _toList() { - return [ - url, - status, - headers, - body, - ]; + return [url, status, headers, body]; } Object encode() { - return _toList(); } + return _toList(); + } static GeckoFetchResponse decode(Object result) { result as List; @@ -5080,7 +5320,10 @@ class GeckoFetchResponse { if (identical(this, other)) { return true; } - return _deepEquals(url, other.url) && _deepEquals(status, other.status) && _deepEquals(headers, other.headers) && _deepEquals(body, other.body); + return _deepEquals(url, other.url) && + _deepEquals(status, other.status) && + _deepEquals(headers, other.headers) && + _deepEquals(body, other.body); } @override @@ -5134,7 +5377,8 @@ class BookmarkNode { } Object encode() { - return _toList(); } + return _toList(); + } static BookmarkNode decode(Object result) { result as List; @@ -5160,7 +5404,15 @@ class BookmarkNode { if (identical(this, other)) { return true; } - return _deepEquals(type, other.type) && _deepEquals(guid, other.guid) && _deepEquals(parentGuid, other.parentGuid) && _deepEquals(position, other.position) && _deepEquals(title, other.title) && _deepEquals(url, other.url) && _deepEquals(dateAdded, other.dateAdded) && _deepEquals(lastModified, other.lastModified) && _deepEquals(children, other.children); + return _deepEquals(type, other.type) && + _deepEquals(guid, other.guid) && + _deepEquals(parentGuid, other.parentGuid) && + _deepEquals(position, other.position) && + _deepEquals(title, other.title) && + _deepEquals(url, other.url) && + _deepEquals(dateAdded, other.dateAdded) && + _deepEquals(lastModified, other.lastModified) && + _deepEquals(children, other.children); } @override @@ -5170,12 +5422,7 @@ class BookmarkNode { /// Class for making alterations to any bookmark node class BookmarkInfo { - BookmarkInfo({ - this.parentGuid, - this.position, - this.title, - this.url, - }); + BookmarkInfo({this.parentGuid, this.position, this.title, this.url}); String? parentGuid; @@ -5186,16 +5433,12 @@ class BookmarkInfo { String? url; List _toList() { - return [ - parentGuid, - position, - title, - url, - ]; + return [parentGuid, position, title, url]; } Object encode() { - return _toList(); } + return _toList(); + } static BookmarkInfo decode(Object result) { result as List; @@ -5216,7 +5459,10 @@ class BookmarkInfo { if (identical(this, other)) { return true; } - return _deepEquals(parentGuid, other.parentGuid) && _deepEquals(position, other.position) && _deepEquals(title, other.title) && _deepEquals(url, other.url); + return _deepEquals(parentGuid, other.parentGuid) && + _deepEquals(position, other.position) && + _deepEquals(title, other.title) && + _deepEquals(url, other.url); } @override @@ -5287,7 +5533,8 @@ class SitePermissions { } Object encode() { - return _toList(); } + return _toList(); + } static SitePermissions decode(Object result) { result as List; @@ -5317,7 +5564,19 @@ class SitePermissions { if (identical(this, other)) { return true; } - return _deepEquals(origin, other.origin) && _deepEquals(camera, other.camera) && _deepEquals(microphone, other.microphone) && _deepEquals(location, other.location) && _deepEquals(notification, other.notification) && _deepEquals(persistentStorage, other.persistentStorage) && _deepEquals(crossOriginStorageAccess, other.crossOriginStorageAccess) && _deepEquals(mediaKeySystemAccess, other.mediaKeySystemAccess) && _deepEquals(localDeviceAccess, other.localDeviceAccess) && _deepEquals(localNetworkAccess, other.localNetworkAccess) && _deepEquals(autoplayAudible, other.autoplayAudible) && _deepEquals(autoplayInaudible, other.autoplayInaudible) && _deepEquals(savedAt, other.savedAt); + return _deepEquals(origin, other.origin) && + _deepEquals(camera, other.camera) && + _deepEquals(microphone, other.microphone) && + _deepEquals(location, other.location) && + _deepEquals(notification, other.notification) && + _deepEquals(persistentStorage, other.persistentStorage) && + _deepEquals(crossOriginStorageAccess, other.crossOriginStorageAccess) && + _deepEquals(mediaKeySystemAccess, other.mediaKeySystemAccess) && + _deepEquals(localDeviceAccess, other.localDeviceAccess) && + _deepEquals(localNetworkAccess, other.localNetworkAccess) && + _deepEquals(autoplayAudible, other.autoplayAudible) && + _deepEquals(autoplayInaudible, other.autoplayInaudible) && + _deepEquals(savedAt, other.savedAt); } @override @@ -5330,32 +5589,28 @@ class SitePermissions { /// This represents a site that has been added to the exceptions list, /// meaning tracking protection is disabled for this specific site. class TrackingProtectionException { - TrackingProtectionException({ - required this.url, - }); + TrackingProtectionException({required this.url}); String url; List _toList() { - return [ - url, - ]; + return [url]; } Object encode() { - return _toList(); } + return _toList(); + } static TrackingProtectionException decode(Object result) { result as List; - return TrackingProtectionException( - url: result[0]! as String, - ); + return TrackingProtectionException(url: result[0]! as String); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! TrackingProtectionException || other.runtimeType != runtimeType) { + if (other is! TrackingProtectionException || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -5371,11 +5626,7 @@ class TrackingProtectionException { /// Represents an icon from a PWA manifest. class PwaIcon { - PwaIcon({ - required this.src, - this.sizes, - this.type, - }); + PwaIcon({required this.src, this.sizes, this.type}); String src; @@ -5384,15 +5635,12 @@ class PwaIcon { String? type; List _toList() { - return [ - src, - sizes, - type, - ]; + return [src, sizes, type]; } Object encode() { - return _toList(); } + return _toList(); + } static PwaIcon decode(Object result) { result as List; @@ -5412,7 +5660,9 @@ class PwaIcon { if (identical(this, other)) { return true; } - return _deepEquals(src, other.src) && _deepEquals(sizes, other.sizes) && _deepEquals(type, other.type); + return _deepEquals(src, other.src) && + _deepEquals(sizes, other.sizes) && + _deepEquals(type, other.type); } @override @@ -5422,24 +5672,19 @@ class PwaIcon { /// Represents a file entry in share target params. class ShareTargetFiles { - ShareTargetFiles({ - required this.name, - required this.accept, - }); + ShareTargetFiles({required this.name, required this.accept}); String name; List accept; List _toList() { - return [ - name, - accept, - ]; + return [name, accept]; } Object encode() { - return _toList(); } + return _toList(); + } static ShareTargetFiles decode(Object result) { result as List; @@ -5468,12 +5713,7 @@ class ShareTargetFiles { /// Represents share target params. class ShareTargetParams { - ShareTargetParams({ - this.title, - this.text, - this.url, - required this.files, - }); + ShareTargetParams({this.title, this.text, this.url, required this.files}); String? title; @@ -5484,16 +5724,12 @@ class ShareTargetParams { List files; List _toList() { - return [ - title, - text, - url, - files, - ]; + return [title, text, url, files]; } Object encode() { - return _toList(); } + return _toList(); + } static ShareTargetParams decode(Object result) { result as List; @@ -5514,7 +5750,10 @@ class ShareTargetParams { if (identical(this, other)) { return true; } - return _deepEquals(title, other.title) && _deepEquals(text, other.text) && _deepEquals(url, other.url) && _deepEquals(files, other.files); + return _deepEquals(title, other.title) && + _deepEquals(text, other.text) && + _deepEquals(url, other.url) && + _deepEquals(files, other.files); } @override @@ -5524,12 +5763,7 @@ class ShareTargetParams { /// Represents a share target for PWA. class ShareTarget { - ShareTarget({ - required this.action, - this.method, - this.encType, - this.params, - }); + ShareTarget({required this.action, this.method, this.encType, this.params}); String action; @@ -5540,16 +5774,12 @@ class ShareTarget { ShareTargetParams? params; List _toList() { - return [ - action, - method, - encType, - params, - ]; + return [action, method, encType, params]; } Object encode() { - return _toList(); } + return _toList(); + } static ShareTarget decode(Object result) { result as List; @@ -5570,7 +5800,10 @@ class ShareTarget { if (identical(this, other)) { return true; } - return _deepEquals(action, other.action) && _deepEquals(method, other.method) && _deepEquals(encType, other.encType) && _deepEquals(params, other.params); + return _deepEquals(action, other.action) && + _deepEquals(method, other.method) && + _deepEquals(encType, other.encType) && + _deepEquals(params, other.params); } @override @@ -5596,16 +5829,12 @@ class ExternalApplicationResource { String? minVersion; List _toList() { - return [ - platform, - url, - id, - minVersion, - ]; + return [platform, url, id, minVersion]; } Object encode() { - return _toList(); } + return _toList(); + } static ExternalApplicationResource decode(Object result) { result as List; @@ -5620,13 +5849,17 @@ class ExternalApplicationResource { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! ExternalApplicationResource || other.runtimeType != runtimeType) { + if (other is! ExternalApplicationResource || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { return true; } - return _deepEquals(platform, other.platform) && _deepEquals(url, other.url) && _deepEquals(id, other.id) && _deepEquals(minVersion, other.minVersion); + return _deepEquals(platform, other.platform) && + _deepEquals(url, other.url) && + _deepEquals(id, other.id) && + _deepEquals(minVersion, other.minVersion); } @override @@ -5731,7 +5964,8 @@ class PwaManifest { } Object encode() { - return _toList(); } + return _toList(); + } static PwaManifest decode(Object result) { result as List; @@ -5748,7 +5982,8 @@ class PwaManifest { dir: result[9] as String?, lang: result[10] as String?, orientation: result[11] as String?, - relatedApplications: (result[12]! as List).cast(), + relatedApplications: (result[12]! as List) + .cast(), preferRelatedApplications: result[13]! as bool, shareTarget: result[14] as ShareTarget?, currentUrl: result[15]! as String, @@ -5766,7 +6001,27 @@ class PwaManifest { if (identical(this, other)) { return true; } - return _deepEquals(startUrl, other.startUrl) && _deepEquals(name, other.name) && _deepEquals(shortName, other.shortName) && _deepEquals(display, other.display) && _deepEquals(themeColor, other.themeColor) && _deepEquals(backgroundColor, other.backgroundColor) && _deepEquals(scope, other.scope) && _deepEquals(description, other.description) && _deepEquals(icons, other.icons) && _deepEquals(dir, other.dir) && _deepEquals(lang, other.lang) && _deepEquals(orientation, other.orientation) && _deepEquals(relatedApplications, other.relatedApplications) && _deepEquals(preferRelatedApplications, other.preferRelatedApplications) && _deepEquals(shareTarget, other.shareTarget) && _deepEquals(currentUrl, other.currentUrl) && _deepEquals(contextId, other.contextId) && _deepEquals(installLabel, other.installLabel); + return _deepEquals(startUrl, other.startUrl) && + _deepEquals(name, other.name) && + _deepEquals(shortName, other.shortName) && + _deepEquals(display, other.display) && + _deepEquals(themeColor, other.themeColor) && + _deepEquals(backgroundColor, other.backgroundColor) && + _deepEquals(scope, other.scope) && + _deepEquals(description, other.description) && + _deepEquals(icons, other.icons) && + _deepEquals(dir, other.dir) && + _deepEquals(lang, other.lang) && + _deepEquals(orientation, other.orientation) && + _deepEquals(relatedApplications, other.relatedApplications) && + _deepEquals( + preferRelatedApplications, + other.preferRelatedApplications, + ) && + _deepEquals(shareTarget, other.shareTarget) && + _deepEquals(currentUrl, other.currentUrl) && + _deepEquals(contextId, other.contextId) && + _deepEquals(installLabel, other.installLabel); } @override @@ -5804,17 +6059,12 @@ class SandboxCaptureEntry { String status; List _toList() { - return [ - tabId, - captureId, - sourceUrl, - redirectUrl, - status, - ]; + return [tabId, captureId, sourceUrl, redirectUrl, status]; } Object encode() { - return _toList(); } + return _toList(); + } static SandboxCaptureEntry decode(Object result) { result as List; @@ -5836,7 +6086,11 @@ class SandboxCaptureEntry { if (identical(this, other)) { return true; } - return _deepEquals(tabId, other.tabId) && _deepEquals(captureId, other.captureId) && _deepEquals(sourceUrl, other.sourceUrl) && _deepEquals(redirectUrl, other.redirectUrl) && _deepEquals(status, other.status); + return _deepEquals(tabId, other.tabId) && + _deepEquals(captureId, other.captureId) && + _deepEquals(sourceUrl, other.sourceUrl) && + _deepEquals(redirectUrl, other.redirectUrl) && + _deepEquals(status, other.status); } @override @@ -5844,7 +6098,6 @@ class SandboxCaptureEntry { int get hashCode => _deepHash([runtimeType, ..._toList()]); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -5852,369 +6105,375 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is RestoreLocation) { + } else if (value is RestoreLocation) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is IconType) { + } else if (value is IconType) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is IconSize) { + } else if (value is IconSize) { buffer.putUint8(131); writeValue(buffer, value.index); - } else if (value is IconSource) { + } else if (value is IconSource) { buffer.putUint8(132); writeValue(buffer, value.index); - } else if (value is CookieSameSiteStatus) { + } else if (value is CookieSameSiteStatus) { buffer.putUint8(133); writeValue(buffer, value.index); - } else if (value is VisitType) { + } else if (value is VisitType) { buffer.putUint8(134); writeValue(buffer, value.index); - } else if (value is FrecencyThresholdOption) { + } else if (value is FrecencyThresholdOption) { buffer.putUint8(135); writeValue(buffer, value.index); - } else if (value is DocumentType) { + } else if (value is DocumentType) { buffer.putUint8(136); writeValue(buffer, value.index); - } else if (value is SelectionPattern) { + } else if (value is SelectionPattern) { buffer.putUint8(137); writeValue(buffer, value.index); - } else if (value is WebExtensionActionType) { + } else if (value is WebExtensionActionType) { buffer.putUint8(138); writeValue(buffer, value.index); - } else if (value is AddonDisabledReason) { + } else if (value is AddonDisabledReason) { buffer.putUint8(139); writeValue(buffer, value.index); - } else if (value is AddonIncognito) { + } else if (value is AddonIncognito) { buffer.putUint8(140); writeValue(buffer, value.index); - } else if (value is AddonUpdateStatus) { + } else if (value is AddonUpdateStatus) { buffer.putUint8(141); writeValue(buffer, value.index); - } else if (value is AddonStoreApp) { + } else if (value is AddonStoreApp) { buffer.putUint8(142); writeValue(buffer, value.index); - } else if (value is AddonStorePromoted) { + } else if (value is AddonStorePromoted) { buffer.putUint8(143); writeValue(buffer, value.index); - } else if (value is GeckoSuggestionType) { + } else if (value is GeckoSuggestionType) { buffer.putUint8(144); writeValue(buffer, value.index); - } else if (value is TrackingProtectionPolicy) { + } else if (value is TrackingProtectionPolicy) { buffer.putUint8(145); writeValue(buffer, value.index); - } else if (value is HttpsOnlyMode) { + } else if (value is HttpsOnlyMode) { buffer.putUint8(146); writeValue(buffer, value.index); - } else if (value is QueryParameterStripping) { + } else if (value is QueryParameterStripping) { buffer.putUint8(147); writeValue(buffer, value.index); - } else if (value is BounceTrackingProtectionMode) { + } else if (value is BounceTrackingProtectionMode) { buffer.putUint8(148); writeValue(buffer, value.index); - } else if (value is ColorScheme) { + } else if (value is ColorScheme) { buffer.putUint8(149); writeValue(buffer, value.index); - } else if (value is CookieBannerHandlingMode) { + } else if (value is CookieBannerHandlingMode) { buffer.putUint8(150); writeValue(buffer, value.index); - } else if (value is AppLinksMode) { + } else if (value is AppLinksMode) { buffer.putUint8(151); writeValue(buffer, value.index); - } else if (value is WebContentIsolationStrategy) { + } else if (value is WebContentIsolationStrategy) { buffer.putUint8(152); writeValue(buffer, value.index); - } else if (value is CustomCookiePolicy) { + } else if (value is CustomCookiePolicy) { buffer.putUint8(153); writeValue(buffer, value.index); - } else if (value is TrackingScope) { + } else if (value is TrackingScope) { buffer.putUint8(154); writeValue(buffer, value.index); - } else if (value is DohSettingsMode) { + } else if (value is DohSettingsMode) { buffer.putUint8(155); writeValue(buffer, value.index); - } else if (value is DownloadStatus) { + } else if (value is DownloadStatus) { buffer.putUint8(156); writeValue(buffer, value.index); - } else if (value is LogLevel) { + } else if (value is LogLevel) { buffer.putUint8(157); writeValue(buffer, value.index); - } else if (value is SyncEngineValue) { + } else if (value is SyncEngineValue) { buffer.putUint8(158); writeValue(buffer, value.index); - } else if (value is MlProgressType) { + } else if (value is MlProgressType) { buffer.putUint8(159); writeValue(buffer, value.index); - } else if (value is MlProgressStatus) { + } else if (value is MlProgressStatus) { buffer.putUint8(160); writeValue(buffer, value.index); - } else if (value is ClearDataType) { + } else if (value is ClearDataType) { buffer.putUint8(161); writeValue(buffer, value.index); - } else if (value is GeckoFetchMethod) { + } else if (value is GeckoFetchMethod) { buffer.putUint8(162); writeValue(buffer, value.index); - } else if (value is GeckoFetchRedircet) { + } else if (value is GeckoFetchRedircet) { buffer.putUint8(163); writeValue(buffer, value.index); - } else if (value is GeckoFetchCookiePolicy) { + } else if (value is GeckoFetchCookiePolicy) { buffer.putUint8(164); writeValue(buffer, value.index); - } else if (value is BookmarkNodeType) { + } else if (value is BookmarkNodeType) { buffer.putUint8(165); writeValue(buffer, value.index); - } else if (value is SitePermissionStatus) { + } else if (value is SitePermissionStatus) { buffer.putUint8(166); writeValue(buffer, value.index); - } else if (value is AutoplayStatus) { + } else if (value is AutoplayStatus) { buffer.putUint8(167); writeValue(buffer, value.index); - } else if (value is TranslationOptions) { + } else if (value is TranslationOptions) { buffer.putUint8(168); writeValue(buffer, value.encode()); - } else if (value is TranslationLanguage) { + } else if (value is TranslationLanguage) { buffer.putUint8(169); writeValue(buffer, value.encode()); - } else if (value is TranslationDetectedLanguages) { + } else if (value is TranslationDetectedLanguages) { buffer.putUint8(170); writeValue(buffer, value.encode()); - } else if (value is TranslationPair) { + } else if (value is TranslationPair) { buffer.putUint8(171); writeValue(buffer, value.encode()); - } else if (value is TranslationEngineStateData) { + } else if (value is TranslationEngineStateData) { buffer.putUint8(172); writeValue(buffer, value.encode()); - } else if (value is TabTranslationStateData) { + } else if (value is TabTranslationStateData) { buffer.putUint8(173); writeValue(buffer, value.encode()); - } else if (value is ReaderState) { + } else if (value is ReaderState) { buffer.putUint8(174); writeValue(buffer, value.encode()); - } else if (value is AddTabParams) { + } else if (value is AddTabParams) { buffer.putUint8(175); writeValue(buffer, value.encode()); - } else if (value is LastMediaAccessState) { + } else if (value is LastMediaAccessState) { buffer.putUint8(176); writeValue(buffer, value.encode()); - } else if (value is HistoryMetadataKey) { + } else if (value is HistoryMetadataKey) { buffer.putUint8(177); writeValue(buffer, value.encode()); - } else if (value is PackageCategoryValue) { + } else if (value is PackageCategoryValue) { buffer.putUint8(178); writeValue(buffer, value.encode()); - } else if (value is ExternalPackage) { + } else if (value is ExternalPackage) { buffer.putUint8(179); writeValue(buffer, value.encode()); - } else if (value is LoadUrlFlagsValue) { + } else if (value is LoadUrlFlagsValue) { buffer.putUint8(180); writeValue(buffer, value.encode()); - } else if (value is SourceValue) { + } else if (value is SourceValue) { buffer.putUint8(181); writeValue(buffer, value.encode()); - } else if (value is TabState) { + } else if (value is TabState) { buffer.putUint8(182); writeValue(buffer, value.encode()); - } else if (value is RecoverableTab) { + } else if (value is RecoverableTab) { buffer.putUint8(183); writeValue(buffer, value.encode()); - } else if (value is IconRequest) { + } else if (value is IconRequest) { buffer.putUint8(184); writeValue(buffer, value.encode()); - } else if (value is ResourceSize) { + } else if (value is ResourceSize) { buffer.putUint8(185); writeValue(buffer, value.encode()); - } else if (value is Resource) { + } else if (value is Resource) { buffer.putUint8(186); writeValue(buffer, value.encode()); - } else if (value is IconResult) { + } else if (value is IconResult) { buffer.putUint8(187); writeValue(buffer, value.encode()); - } else if (value is CookiePartitionKey) { + } else if (value is CookiePartitionKey) { buffer.putUint8(188); writeValue(buffer, value.encode()); - } else if (value is Cookie) { + } else if (value is Cookie) { buffer.putUint8(189); writeValue(buffer, value.encode()); - } else if (value is VisitInfo) { + } else if (value is VisitInfo) { buffer.putUint8(190); writeValue(buffer, value.encode()); - } else if (value is HistoryHighlightWeights) { + } else if (value is HistoryHighlightWeights) { buffer.putUint8(191); writeValue(buffer, value.encode()); - } else if (value is HistoryHighlight) { + } else if (value is HistoryHighlight) { buffer.putUint8(192); writeValue(buffer, value.encode()); - } else if (value is TopFrecentSiteInfo) { + } else if (value is TopFrecentSiteInfo) { buffer.putUint8(193); writeValue(buffer, value.encode()); - } else if (value is HistoryMetadata) { + } else if (value is HistoryMetadata) { buffer.putUint8(194); writeValue(buffer, value.encode()); - } else if (value is HistorySuggestion) { + } else if (value is HistorySuggestion) { buffer.putUint8(195); writeValue(buffer, value.encode()); - } else if (value is PageObservation) { + } else if (value is PageObservation) { buffer.putUint8(196); writeValue(buffer, value.encode()); - } else if (value is HistoryItem) { + } else if (value is HistoryItem) { buffer.putUint8(197); writeValue(buffer, value.encode()); - } else if (value is HistoryState) { + } else if (value is HistoryState) { buffer.putUint8(198); writeValue(buffer, value.encode()); - } else if (value is ReaderableState) { + } else if (value is ReaderableState) { buffer.putUint8(199); writeValue(buffer, value.encode()); - } else if (value is SecurityInfoState) { + } else if (value is SecurityInfoState) { buffer.putUint8(200); writeValue(buffer, value.encode()); - } else if (value is TabContentState) { + } else if (value is TabContentState) { buffer.putUint8(201); writeValue(buffer, value.encode()); - } else if (value is FindResultState) { + } else if (value is FindResultState) { buffer.putUint8(202); writeValue(buffer, value.encode()); - } else if (value is CustomSelectionAction) { + } else if (value is CustomSelectionAction) { buffer.putUint8(203); writeValue(buffer, value.encode()); - } else if (value is WebExtensionData) { + } else if (value is WebExtensionData) { buffer.putUint8(204); writeValue(buffer, value.encode()); - } else if (value is AddonInfo) { + } else if (value is AddonInfo) { buffer.putUint8(205); writeValue(buffer, value.encode()); - } else if (value is AddonListingPreview) { + } else if (value is AddonListingPreview) { buffer.putUint8(206); writeValue(buffer, value.encode()); - } else if (value is AddonListing) { + } else if (value is AddonListing) { buffer.putUint8(207); writeValue(buffer, value.encode()); - } else if (value is AddonStoreInfo) { + } else if (value is AddonStoreInfo) { buffer.putUint8(208); writeValue(buffer, value.encode()); - } else if (value is AddonUpdateAttemptInfo) { + } else if (value is AddonUpdateAttemptInfo) { buffer.putUint8(209); writeValue(buffer, value.encode()); - } else if (value is GeckoSuggestion) { + } else if (value is GeckoSuggestion) { buffer.putUint8(210); writeValue(buffer, value.encode()); - } else if (value is TabContent) { + } else if (value is TabContent) { buffer.putUint8(211); writeValue(buffer, value.encode()); - } else if (value is ContentBlocking) { + } else if (value is ContentBlocking) { buffer.putUint8(212); writeValue(buffer, value.encode()); - } else if (value is DohSettings) { + } else if (value is DohSettings) { buffer.putUint8(213); writeValue(buffer, value.encode()); - } else if (value is GeckoEngineSettings) { + } else if (value is GeckoEngineSettings) { buffer.putUint8(214); writeValue(buffer, value.encode()); - } else if (value is AutocompleteResult) { + } else if (value is AutocompleteResult) { buffer.putUint8(215); writeValue(buffer, value.encode()); - } else if (value is UnknownHitResult) { + } else if (value is UnknownHitResult) { buffer.putUint8(216); writeValue(buffer, value.encode()); - } else if (value is ImageHitResult) { + } else if (value is ImageHitResult) { buffer.putUint8(217); writeValue(buffer, value.encode()); - } else if (value is VideoHitResult) { + } else if (value is VideoHitResult) { buffer.putUint8(218); writeValue(buffer, value.encode()); - } else if (value is AudioHitResult) { + } else if (value is AudioHitResult) { buffer.putUint8(219); writeValue(buffer, value.encode()); - } else if (value is ImageSrcHitResult) { + } else if (value is ImageSrcHitResult) { buffer.putUint8(220); writeValue(buffer, value.encode()); - } else if (value is PhoneHitResult) { + } else if (value is PhoneHitResult) { buffer.putUint8(221); writeValue(buffer, value.encode()); - } else if (value is EmailHitResult) { + } else if (value is EmailHitResult) { buffer.putUint8(222); writeValue(buffer, value.encode()); - } else if (value is GeoHitResult) { + } else if (value is GeoHitResult) { buffer.putUint8(223); writeValue(buffer, value.encode()); - } else if (value is DownloadState) { + } else if (value is DownloadState) { buffer.putUint8(224); writeValue(buffer, value.encode()); - } else if (value is ShareInternetResourceState) { + } else if (value is ShareInternetResourceState) { buffer.putUint8(225); writeValue(buffer, value.encode()); - } else if (value is AddonCollection) { + } else if (value is AddonCollection) { buffer.putUint8(226); writeValue(buffer, value.encode()); - } else if (value is SyncEngineStatus) { + } else if (value is SyncEngineStatus) { buffer.putUint8(227); writeValue(buffer, value.encode()); - } else if (value is SyncAccountInfo) { + } else if (value is SyncAccountInfo) { buffer.putUint8(228); writeValue(buffer, value.encode()); - } else if (value is SyncDevice) { + } else if (value is SyncDevice) { buffer.putUint8(229); writeValue(buffer, value.encode()); - } else if (value is SyncIncomingTab) { + } else if (value is SyncIncomingTab) { buffer.putUint8(230); writeValue(buffer, value.encode()); - } else if (value is SyncRemoteTab) { + } else if (value is SyncRemoteTab) { buffer.putUint8(231); writeValue(buffer, value.encode()); - } else if (value is SyncDeviceTabs) { + } else if (value is SyncDeviceTabs) { buffer.putUint8(232); writeValue(buffer, value.encode()); - } else if (value is GeckoPref) { + } else if (value is GeckoPref) { buffer.putUint8(233); writeValue(buffer, value.encode()); - } else if (value is MlProgressData) { + } else if (value is MlProgressData) { buffer.putUint8(234); writeValue(buffer, value.encode()); - } else if (value is ContainerSiteAssignment) { + } else if (value is GeckoProxySettings) { buffer.putUint8(235); writeValue(buffer, value.encode()); - } else if (value is GeckoHeader) { + } else if (value is ContainerSiteAssignment) { buffer.putUint8(236); writeValue(buffer, value.encode()); - } else if (value is GeckoFetchRequest) { + } else if (value is ProxyLoadError) { buffer.putUint8(237); writeValue(buffer, value.encode()); - } else if (value is GeckoFetchResponse) { + } else if (value is GeckoHeader) { buffer.putUint8(238); writeValue(buffer, value.encode()); - } else if (value is BookmarkNode) { + } else if (value is GeckoFetchRequest) { buffer.putUint8(239); writeValue(buffer, value.encode()); - } else if (value is BookmarkInfo) { + } else if (value is GeckoFetchResponse) { buffer.putUint8(240); writeValue(buffer, value.encode()); - } else if (value is SitePermissions) { + } else if (value is BookmarkNode) { buffer.putUint8(241); writeValue(buffer, value.encode()); - } else if (value is TrackingProtectionException) { + } else if (value is BookmarkInfo) { buffer.putUint8(242); writeValue(buffer, value.encode()); - } else if (value is PwaIcon) { + } else if (value is SitePermissions) { buffer.putUint8(243); writeValue(buffer, value.encode()); - } else if (value is ShareTargetFiles) { + } else if (value is TrackingProtectionException) { buffer.putUint8(244); writeValue(buffer, value.encode()); - } else if (value is ShareTargetParams) { + } else if (value is PwaIcon) { buffer.putUint8(245); writeValue(buffer, value.encode()); - } else if (value is ShareTarget) { + } else if (value is ShareTargetFiles) { buffer.putUint8(246); writeValue(buffer, value.encode()); - } else if (value is ExternalApplicationResource) { + } else if (value is ShareTargetParams) { buffer.putUint8(247); writeValue(buffer, value.encode()); - } else if (value is PwaManifest) { + } else if (value is ShareTarget) { buffer.putUint8(248); writeValue(buffer, value.encode()); - } else if (value is SandboxCaptureEntry) { + } else if (value is ExternalApplicationResource) { buffer.putUint8(249); writeValue(buffer, value.encode()); + } else if (value is PwaManifest) { + buffer.putUint8(250); + writeValue(buffer, value.encode()); + } else if (value is SandboxCaptureEntry) { + buffer.putUint8(251); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -6282,7 +6541,9 @@ class _PigeonCodec extends StandardMessageCodec { return value == null ? null : QueryParameterStripping.values[value]; case 148: final value = readValue(buffer) as int?; - return value == null ? null : BounceTrackingProtectionMode.values[value]; + return value == null + ? null + : BounceTrackingProtectionMode.values[value]; case 149: final value = readValue(buffer) as int?; return value == null ? null : ColorScheme.values[value]; @@ -6475,34 +6736,38 @@ class _PigeonCodec extends StandardMessageCodec { case 234: return MlProgressData.decode(readValue(buffer)!); case 235: - return ContainerSiteAssignment.decode(readValue(buffer)!); + return GeckoProxySettings.decode(readValue(buffer)!); case 236: - return GeckoHeader.decode(readValue(buffer)!); + return ContainerSiteAssignment.decode(readValue(buffer)!); case 237: - return GeckoFetchRequest.decode(readValue(buffer)!); + return ProxyLoadError.decode(readValue(buffer)!); case 238: - return GeckoFetchResponse.decode(readValue(buffer)!); + return GeckoHeader.decode(readValue(buffer)!); case 239: - return BookmarkNode.decode(readValue(buffer)!); + return GeckoFetchRequest.decode(readValue(buffer)!); case 240: - return BookmarkInfo.decode(readValue(buffer)!); + return GeckoFetchResponse.decode(readValue(buffer)!); case 241: - return SitePermissions.decode(readValue(buffer)!); + return BookmarkNode.decode(readValue(buffer)!); case 242: - return TrackingProtectionException.decode(readValue(buffer)!); + return BookmarkInfo.decode(readValue(buffer)!); case 243: - return PwaIcon.decode(readValue(buffer)!); + return SitePermissions.decode(readValue(buffer)!); case 244: - return ShareTargetFiles.decode(readValue(buffer)!); + return TrackingProtectionException.decode(readValue(buffer)!); case 245: - return ShareTargetParams.decode(readValue(buffer)!); + return PwaIcon.decode(readValue(buffer)!); case 246: - return ShareTarget.decode(readValue(buffer)!); + return ShareTargetFiles.decode(readValue(buffer)!); case 247: - return ExternalApplicationResource.decode(readValue(buffer)!); + return ShareTargetParams.decode(readValue(buffer)!); case 248: - return PwaManifest.decode(readValue(buffer)!); + return ShareTarget.decode(readValue(buffer)!); case 249: + return ExternalApplicationResource.decode(readValue(buffer)!); + case 250: + return PwaManifest.decode(readValue(buffer)!); + case 251: return SandboxCaptureEntry.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -6514,9 +6779,13 @@ class GeckoBrowserApi { /// Constructor for [GeckoBrowserApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoBrowserApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoBrowserApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -6524,7 +6793,8 @@ class GeckoBrowserApi { final String pigeonVar_messageChannelSuffix; Future getGeckoVersion() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.getGeckoVersion$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.getGeckoVersion$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6534,34 +6804,55 @@ class GeckoBrowserApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } - Future initialize(String profileFolder, LogLevel logLevel, ContentBlocking contentBlocking, AddonCollection? addonCollection, String? fxaServerOverride, String? syncTokenServerOverride, GeckoEngineSettings? startupSettings, String? startupUBlockFilterListsPref, bool clearStartupUBlockFilterListsPref) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.initialize$pigeonVar_messageChannelSuffix'; + Future initialize( + String profileFolder, + LogLevel logLevel, + ContentBlocking contentBlocking, + AddonCollection? addonCollection, + String? fxaServerOverride, + String? syncTokenServerOverride, + GeckoEngineSettings? startupSettings, + String? startupUBlockFilterListsPref, + bool clearStartupUBlockFilterListsPref, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([profileFolder, logLevel, contentBlocking, addonCollection, fxaServerOverride, syncTokenServerOverride, startupSettings, startupUBlockFilterListsPref, clearStartupUBlockFilterListsPref]); + final Future pigeonVar_sendFuture = pigeonVar_channel + .send([ + profileFolder, + logLevel, + contentBlocking, + addonCollection, + fxaServerOverride, + syncTokenServerOverride, + startupSettings, + startupUBlockFilterListsPref, + clearStartupUBlockFilterListsPref, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future showNativeFragment() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.showNativeFragment$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.showNativeFragment$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6571,52 +6862,60 @@ class GeckoBrowserApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } Future onTrimMemory(int level) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.onTrimMemory$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.onTrimMemory$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([level]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [level], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future openInCustomTab({required String url, required bool private, required String? contextId, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.openInCustomTab$pigeonVar_messageChannelSuffix'; + Future openInCustomTab({ + required String url, + required bool private, + required String? contextId, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.openInCustomTab$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, private, contextId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url, private, contextId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future isDefaultBrowser() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.isDefaultBrowser$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.isDefaultBrowser$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6626,16 +6925,16 @@ class GeckoBrowserApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } Future requestDefaultBrowser() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.requestDefaultBrowser$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.requestDefaultBrowser$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6645,15 +6944,15 @@ class GeckoBrowserApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future pickUnifiedPushDistributor() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.pickUnifiedPushDistributor$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.pickUnifiedPushDistributor$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6663,16 +6962,16 @@ class GeckoBrowserApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } Future shutdown() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.shutdown$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.shutdown$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6682,11 +6981,10 @@ class GeckoBrowserApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -6694,9 +6992,13 @@ class GeckoSyncApi { /// Constructor for [GeckoSyncApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoSyncApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoSyncApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -6704,7 +7006,8 @@ class GeckoSyncApi { final String pigeonVar_messageChannelSuffix; Future getAccountInfo() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.getAccountInfo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.getAccountInfo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6714,16 +7017,16 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as SyncAccountInfo; } Future beginAuthentication() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.beginAuthentication$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.beginAuthentication$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6733,33 +7036,35 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future beginPairingAuthentication(String pairingUrl) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.beginPairingAuthentication$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.beginPairingAuthentication$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([pairingUrl]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [pairingUrl], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future logout() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.logout$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.logout$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6769,15 +7074,15 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future syncNow() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.syncNow$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.syncNow$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6787,33 +7092,35 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future setEngineEnabled(SyncEngineValue engine, bool enabled) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.setEngineEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.setEngineEnabled$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([engine, enabled]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [engine, enabled], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future> getSyncedTabs() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.getSyncedTabs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.getSyncedTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6823,16 +7130,16 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } Future> getDevices() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.getDevices$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.getDevices$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6842,35 +7149,42 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } - Future sendTabToDevice(String deviceId, String title, String url, bool private) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.sendTabToDevice$pigeonVar_messageChannelSuffix'; + Future sendTabToDevice( + String deviceId, + String title, + String url, + bool private, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.sendTabToDevice$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([deviceId, title, url, private]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [deviceId, title, url, private], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } Future refreshDevices() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.refreshDevices$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.refreshDevices$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6880,15 +7194,15 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future pollDeviceCommands() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.pollDeviceCommands$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.pollDeviceCommands$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6898,15 +7212,15 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future> drainIncomingTabs() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.drainIncomingTabs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.drainIncomingTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6916,16 +7230,16 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } Future getDeviceName() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.getDeviceName$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.getDeviceName$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -6935,30 +7249,31 @@ class GeckoSyncApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as String?; } Future setDeviceName(String newName) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.setDeviceName$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncApi.setDeviceName$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([newName]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [newName], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } } @@ -6967,9 +7282,13 @@ class GeckoEngineSettingsApi { /// Constructor for [GeckoEngineSettingsApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoEngineSettingsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoEngineSettingsApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -6977,99 +7296,110 @@ class GeckoEngineSettingsApi { final String pigeonVar_messageChannelSuffix; Future setDefaultSettings(GeckoEngineSettings settings) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setDefaultSettings$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setDefaultSettings$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([settings]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [settings], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future updateRuntimeSettings(GeckoEngineSettings settings) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.updateRuntimeSettings$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.updateRuntimeSettings$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([settings]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [settings], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future setScreenshotProtectionEnabled(bool enabled) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setScreenshotProtectionEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setScreenshotProtectionEnabled$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [enabled], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future setPullToRefreshEnabled(bool enabled) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setPullToRefreshEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setPullToRefreshEnabled$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [enabled], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Sets the app links mode preference (stored in SharedPreferences). /// Controls how external app links are handled in the browser. Future setAppLinksMode(AppLinksMode mode) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setAppLinksMode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setAppLinksMode$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([mode]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [mode], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future getAppLinksMode() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.getAppLinksMode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.getAppLinksMode$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -7079,36 +7409,38 @@ class GeckoEngineSettingsApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as AppLinksMode; } /// Sets whether to use external download managers for downloads. /// When enabled, downloads are forwarded to third-party apps like ADM, 1DM, AB DM. Future setUseExternalDownloadManager(bool enabled) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setUseExternalDownloadManager$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setUseExternalDownloadManager$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [enabled], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future getUseExternalDownloadManager() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.getUseExternalDownloadManager$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.getUseExternalDownloadManager$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -7118,11 +7450,10 @@ class GeckoEngineSettingsApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } } @@ -7131,269 +7462,332 @@ class GeckoSessionApi { /// Constructor for [GeckoSessionApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoSessionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoSessionApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; - Future loadUrl({required String? tabId, required String url, required LoadUrlFlagsValue flags, required Map? additionalHeaders, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.loadUrl$pigeonVar_messageChannelSuffix'; + Future loadUrl({ + required String? tabId, + required String url, + required LoadUrlFlagsValue flags, + required Map? additionalHeaders, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.loadUrl$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, url, flags, additionalHeaders]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, url, flags, additionalHeaders], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future loadData({required String? tabId, required String data, required String mimeType, required String encoding, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.loadData$pigeonVar_messageChannelSuffix'; + Future loadData({ + required String? tabId, + required String data, + required String mimeType, + required String encoding, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.loadData$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, data, mimeType, encoding]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, data, mimeType, encoding], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future reload({required String? tabId, required LoadUrlFlagsValue flags}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.reload$pigeonVar_messageChannelSuffix'; + Future reload({ + required String? tabId, + required LoadUrlFlagsValue flags, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.reload$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, flags]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, flags], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future stopLoading({required String? tabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.stopLoading$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.stopLoading$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future goBack({required String? tabId, required bool userInteraction}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.goBack$pigeonVar_messageChannelSuffix'; + Future goBack({ + required String? tabId, + required bool userInteraction, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.goBack$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, userInteraction]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, userInteraction], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future goForward({required String? tabId, required bool userInteraction}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.goForward$pigeonVar_messageChannelSuffix'; + Future goForward({ + required String? tabId, + required bool userInteraction, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.goForward$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, userInteraction]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, userInteraction], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future goToHistoryIndex({required int index, required String? tabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.goToHistoryIndex$pigeonVar_messageChannelSuffix'; + Future goToHistoryIndex({ + required int index, + required String? tabId, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.goToHistoryIndex$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([index, tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [index, tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future requestDesktopSite({required String? tabId, required bool enable}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.requestDesktopSite$pigeonVar_messageChannelSuffix'; + Future requestDesktopSite({ + required String? tabId, + required bool enable, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.requestDesktopSite$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, enable]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, enable], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future exitFullscreen({required String? tabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.exitFullscreen$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.exitFullscreen$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future saveToPdf({required String? tabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.saveToPdf$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.saveToPdf$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future printContent({required String? tabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.printContent$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.printContent$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future translate({required String? tabId, required String fromLanguage, required String toLanguage, required TranslationOptions? options, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.translate$pigeonVar_messageChannelSuffix'; + Future translate({ + required String? tabId, + required String fromLanguage, + required String toLanguage, + required TranslationOptions? options, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.translate$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, fromLanguage, toLanguage, options]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, fromLanguage, toLanguage, options], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future translateRestore({required String? tabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.translateRestore$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.translateRestore$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future crashRecovery({required List? tabIds}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.crashRecovery$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.crashRecovery$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabIds]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabIds], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future purgeHistory() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.purgeHistory$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.purgeHistory$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -7403,66 +7797,74 @@ class GeckoSessionApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future updateLastAccess({required String? tabId, required int? lastAccess}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.updateLastAccess$pigeonVar_messageChannelSuffix'; + Future updateLastAccess({ + required String? tabId, + required int? lastAccess, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.updateLastAccess$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, lastAccess]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, lastAccess], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future requestScreenshot(bool sendBack) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.requestScreenshot$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.requestScreenshot$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([sendBack]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [sendBack], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as Uint8List?; } Future dispatchKeyEvent({required int keyCode}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.dispatchKeyEvent$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.dispatchKeyEvent$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([keyCode]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [keyCode], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -7470,145 +7872,217 @@ class GeckoTabsApi { /// Constructor for [GeckoTabsApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoTabsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoTabsApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; - Future syncEvents({required bool onSelectedTabChange, required bool onTabListChange, required bool onTabContentStateChange, required bool onIconChange, required bool onSecurityInfoStateChange, required bool onReaderableStateChange, required bool onHistoryStateChange, required bool onFindResults, required bool onThumbnailChange, required bool onBrowserExtensionsChange, required bool onPageExtensionsChange, required bool onBrowserExtensionIcons, required bool onPageExtensionIcons, required bool onTranslationStateChange, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.syncEvents$pigeonVar_messageChannelSuffix'; + Future syncEvents({ + required bool onSelectedTabChange, + required bool onTabListChange, + required bool onTabContentStateChange, + required bool onIconChange, + required bool onSecurityInfoStateChange, + required bool onReaderableStateChange, + required bool onHistoryStateChange, + required bool onFindResults, + required bool onThumbnailChange, + required bool onBrowserExtensionsChange, + required bool onPageExtensionsChange, + required bool onBrowserExtensionIcons, + required bool onPageExtensionIcons, + required bool onTranslationStateChange, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.syncEvents$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([onSelectedTabChange, onTabListChange, onTabContentStateChange, onIconChange, onSecurityInfoStateChange, onReaderableStateChange, onHistoryStateChange, onFindResults, onThumbnailChange, onBrowserExtensionsChange, onPageExtensionsChange, onBrowserExtensionIcons, onPageExtensionIcons, onTranslationStateChange]); + final Future pigeonVar_sendFuture = pigeonVar_channel + .send([ + onSelectedTabChange, + onTabListChange, + onTabContentStateChange, + onIconChange, + onSecurityInfoStateChange, + onReaderableStateChange, + onHistoryStateChange, + onFindResults, + onThumbnailChange, + onBrowserExtensionsChange, + onPageExtensionsChange, + onBrowserExtensionIcons, + onPageExtensionIcons, + onTranslationStateChange, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future selectTab({required String tabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.selectTab$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.selectTab$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future removeTab({required String tabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeTab$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeTab$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future addTab({required String url, required bool selectTab, required bool startLoading, required String? parentId, required LoadUrlFlagsValue flags, required String? contextId, required SourceValue source, required bool private, required HistoryMetadataKey? historyMetadata, required Map? additionalHeaders, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.addTab$pigeonVar_messageChannelSuffix'; + Future addTab({ + required String url, + required bool selectTab, + required bool startLoading, + required String? parentId, + required LoadUrlFlagsValue flags, + required String? contextId, + required SourceValue source, + required bool private, + required HistoryMetadataKey? historyMetadata, + required Map? additionalHeaders, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.addTab$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, selectTab, startLoading, parentId, flags, contextId, source, private, historyMetadata, additionalHeaders]); + final Future pigeonVar_sendFuture = pigeonVar_channel + .send([ + url, + selectTab, + startLoading, + parentId, + flags, + contextId, + source, + private, + historyMetadata, + additionalHeaders, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } - Future> addMultipleTabs({required List tabs, required String? selectTabId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.addMultipleTabs$pigeonVar_messageChannelSuffix'; + Future> addMultipleTabs({ + required List tabs, + required String? selectTabId, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.addMultipleTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabs, selectTabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabs, selectTabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } Future removeAllTabs({required bool recoverable}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeAllTabs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeAllTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([recoverable]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [recoverable], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future removeTabs({required List ids}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeTabs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ids]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [ids], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future removeNormalTabs() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeNormalTabs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeNormalTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -7618,15 +8092,15 @@ class GeckoTabsApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future removePrivateTabs() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removePrivateTabs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removePrivateTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -7636,15 +8110,15 @@ class GeckoTabsApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future undo() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.undo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.undo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -7654,125 +8128,160 @@ class GeckoTabsApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future restoreTabsByList({required List tabs, required String? selectTabId, required RestoreLocation restoreLocation, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.restoreTabsByList$pigeonVar_messageChannelSuffix'; + Future restoreTabsByList({ + required List tabs, + required String? selectTabId, + required RestoreLocation restoreLocation, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.restoreTabsByList$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabs, selectTabId, restoreLocation]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabs, selectTabId, restoreLocation], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Selects an already existing tab with the matching [HistoryMetadataKey] or otherwise /// creates a new tab with the given [url]. - Future selectOrAddTabByHistory({required String url, required HistoryMetadataKey historyMetadata}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.selectOrAddTabByHistory$pigeonVar_messageChannelSuffix'; + Future selectOrAddTabByHistory({ + required String url, + required HistoryMetadataKey historyMetadata, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.selectOrAddTabByHistory$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, historyMetadata]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url, historyMetadata], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } /// Selects an already existing tab displaying [url] or otherwise creates a new tab. - Future selectOrAddTabByUrl({required String url, required bool private, required SourceValue source, required LoadUrlFlagsValue flags, required bool ignoreFragment, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.selectOrAddTabByUrl$pigeonVar_messageChannelSuffix'; + Future selectOrAddTabByUrl({ + required String url, + required bool private, + required SourceValue source, + required LoadUrlFlagsValue flags, + required bool ignoreFragment, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.selectOrAddTabByUrl$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, private, source, flags, ignoreFragment]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url, private, source, flags, ignoreFragment], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } - Future duplicateTab({required String? selectTabId, required bool selectNewTab, required String? newContextId, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.duplicateTab$pigeonVar_messageChannelSuffix'; + Future duplicateTab({ + required String? selectTabId, + required bool selectNewTab, + required String? newContextId, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.duplicateTab$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([selectTabId, selectNewTab, newContextId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [selectTabId, selectNewTab, newContextId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } - Future moveTabs({required List tabIds, required String targetTabId, required bool placeAfter, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.moveTabs$pigeonVar_messageChannelSuffix'; + Future moveTabs({ + required List tabIds, + required String targetTabId, + required bool placeAfter, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.moveTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabIds, targetTabId, placeAfter]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabIds, targetTabId, placeAfter], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future migratePrivateTabUseCase({required String tabId, required String? alternativeUrl}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.migratePrivateTabUseCase$pigeonVar_messageChannelSuffix'; + Future migratePrivateTabUseCase({ + required String tabId, + required String? alternativeUrl, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.migratePrivateTabUseCase$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, alternativeUrl]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, alternativeUrl], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } } @@ -7781,9 +8290,13 @@ class GeckoFindApi { /// Constructor for [GeckoFindApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoFindApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoFindApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -7791,57 +8304,63 @@ class GeckoFindApi { final String pigeonVar_messageChannelSuffix; Future findAll(String? tabId, String text) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.findAll$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.findAll$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, text]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, text], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future findNext(String? tabId, bool forward) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.findNext$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.findNext$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, forward]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, forward], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future clearMatches(String? tabId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.clearMatches$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.clearMatches$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -7849,9 +8368,13 @@ class GeckoIconsApi { /// Constructor for [GeckoIconsApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoIconsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoIconsApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -7859,21 +8382,23 @@ class GeckoIconsApi { final String pigeonVar_messageChannelSuffix; Future loadIcon(IconRequest request) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoIconsApi.loadIcon$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoIconsApi.loadIcon$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([request]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [request], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as IconResult; } } @@ -7882,9 +8407,13 @@ class GeckoPrefApi { /// Constructor for [GeckoPrefApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoPrefApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoPrefApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -7892,63 +8421,72 @@ class GeckoPrefApi { final String pigeonVar_messageChannelSuffix; Future> getPrefs(List preferenceFilter) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.getPrefs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.getPrefs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([preferenceFilter]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [preferenceFilter], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; - return (pigeonVar_replyValue! as Map).cast(); + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as Map) + .cast(); } Future> applyPrefs(Map prefs) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.applyPrefs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.applyPrefs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([prefs]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [prefs], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; - return (pigeonVar_replyValue! as Map).cast(); + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as Map) + .cast(); } Future resetPrefs(List preferenceNames) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.resetPrefs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.resetPrefs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([preferenceNames]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [preferenceNames], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future startObserveChanges() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.startObserveChanges$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.startObserveChanges$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -7958,15 +8496,15 @@ class GeckoPrefApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future stopObserveChanges() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.stopObserveChanges$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.stopObserveChanges$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -7976,47 +8514,50 @@ class GeckoPrefApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future registerPrefForObservation(String name) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.registerPrefForObservation$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.registerPrefForObservation$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([name]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [name], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future unregisterPrefForObservation(String name) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.unregisterPrefForObservation$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPrefApi.unregisterPrefForObservation$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([name]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [name], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -8024,9 +8565,13 @@ class GeckoMlApi { /// Constructor for [GeckoMlApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoMlApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoMlApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -8034,40 +8579,46 @@ class GeckoMlApi { final String pigeonVar_messageChannelSuffix; Future predictDocumentTopic(List documents) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.predictDocumentTopic$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.predictDocumentTopic$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([documents]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [documents], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } - Future> generateDocumentEmbeddings(List documents) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.generateDocumentEmbeddings$pigeonVar_messageChannelSuffix'; + Future> generateDocumentEmbeddings( + List documents, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.generateDocumentEmbeddings$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([documents]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [documents], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as List; } } @@ -8076,9 +8627,13 @@ class GeckoBrowserExtensionApi { /// Constructor for [GeckoBrowserExtensionApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoBrowserExtensionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoBrowserExtensionApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -8086,21 +8641,23 @@ class GeckoBrowserExtensionApi { final String pigeonVar_messageChannelSuffix; Future> getMarkdown(List htmlList) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserExtensionApi.getMarkdown$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserExtensionApi.getMarkdown$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([htmlList]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [htmlList], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } } @@ -8109,9 +8666,13 @@ class GeckoContainerProxyApi { /// Constructor for [GeckoContainerProxyApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoContainerProxyApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoContainerProxyApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -8119,79 +8680,191 @@ class GeckoContainerProxyApi { final String pigeonVar_messageChannelSuffix; Future setProxyPort(int port) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setProxyPort$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setProxyPort$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([port]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [port], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future addContainerProxy(String contextId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.addContainerProxy$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.addContainerProxy$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([contextId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [contextId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future removeContainerProxy(String contextId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.removeContainerProxy$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.removeContainerProxy$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([contextId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [contextId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future upsertProxy(GeckoProxySettings proxy) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.upsertProxy$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [proxy], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future removeProxy(String proxyId) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.removeProxy$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [proxyId], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future setContainerProxy(String contextId, String proxyId) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setContainerProxy$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [contextId, proxyId], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future clearContainerProxy(String contextId) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.clearContainerProxy$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [contextId], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future removeContainerProxyRelation( + String contextId, + String proxyId, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.removeContainerProxyRelation$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [contextId, proxyId], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future setSiteAssignments(Map assignments) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setSiteAssignments$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setSiteAssignments$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([assignments]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [assignments], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future healthcheck() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -8201,11 +8874,10 @@ class GeckoContainerProxyApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } } @@ -8214,87 +8886,143 @@ class GeckoCookieApi { /// Constructor for [GeckoCookieApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoCookieApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoCookieApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; - Future getCookie(String? firstPartyDomain, String name, CookiePartitionKey? partitionKey, String? storeId, String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getCookie$pigeonVar_messageChannelSuffix'; + Future getCookie( + String? firstPartyDomain, + String name, + CookiePartitionKey? partitionKey, + String? storeId, + String url, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getCookie$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([firstPartyDomain, name, partitionKey, storeId, url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [firstPartyDomain, name, partitionKey, storeId, url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Cookie; } - Future> getAllCookies(String? domain, String? firstPartyDomain, String? name, CookiePartitionKey? partitionKey, String? storeId, String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getAllCookies$pigeonVar_messageChannelSuffix'; + Future> getAllCookies( + String? domain, + String? firstPartyDomain, + String? name, + CookiePartitionKey? partitionKey, + String? storeId, + String url, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getAllCookies$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([domain, firstPartyDomain, name, partitionKey, storeId, url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [domain, firstPartyDomain, name, partitionKey, storeId, url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } - Future setCookie(String? domain, int? expirationDate, String? firstPartyDomain, bool? httpOnly, String? name, CookiePartitionKey? partitionKey, String? path, CookieSameSiteStatus? sameSite, bool? secure, String? storeId, String url, String? value) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.setCookie$pigeonVar_messageChannelSuffix'; + Future setCookie( + String? domain, + int? expirationDate, + String? firstPartyDomain, + bool? httpOnly, + String? name, + CookiePartitionKey? partitionKey, + String? path, + CookieSameSiteStatus? sameSite, + bool? secure, + String? storeId, + String url, + String? value, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.setCookie$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([domain, expirationDate, firstPartyDomain, httpOnly, name, partitionKey, path, sameSite, secure, storeId, url, value]); + final Future pigeonVar_sendFuture = pigeonVar_channel + .send([ + domain, + expirationDate, + firstPartyDomain, + httpOnly, + name, + partitionKey, + path, + sameSite, + secure, + storeId, + url, + value, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future removeCookie(String? firstPartyDomain, String name, CookiePartitionKey? partitionKey, String? storeId, String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.removeCookie$pigeonVar_messageChannelSuffix'; + Future removeCookie( + String? firstPartyDomain, + String name, + CookiePartitionKey? partitionKey, + String? storeId, + String url, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.removeCookie$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([firstPartyDomain, name, partitionKey, storeId, url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [firstPartyDomain, name, partitionKey, storeId, url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -8319,7 +9047,11 @@ abstract class GeckoStateEvents { void onReaderableStateChange(int sequence, String id, ReaderableState state); - void onSecurityInfoStateChange(int sequence, String id, SecurityInfoState state); + void onSecurityInfoStateChange( + int sequence, + String id, + SecurityInfoState state, + ); void onIconChange(int sequence, String id, Uint8List? bytes); @@ -8333,20 +9065,33 @@ abstract class GeckoStateEvents { void onContainerSiteAssignment(int sequence, ContainerSiteAssignment details); + void onProxyLoadError(int sequence, ProxyLoadError details); + void onMlProgress(int sequence, MlProgressData progress); void onManifestUpdate(int sequence, String tabId, PwaManifest? manifest); - void onTranslationEngineStateChange(int sequence, TranslationEngineStateData state); + void onTranslationEngineStateChange( + int sequence, + TranslationEngineStateData state, + ); void onTabTranslationStateChange(int sequence, TabTranslationStateData state); - static void setUp(GeckoStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + GeckoStateEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8359,16 +9104,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8381,16 +9130,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconUpdate$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconUpdate$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8404,16 +9157,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8426,38 +9183,47 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { final List args = message! as List; final int arg_sequence = args[0]! as int; - final List arg_tabIds = (args[1]! as List).cast(); + final List arg_tabIds = (args[1]! as List) + .cast(); try { api.onTabListChange(arg_sequence, arg_tabIds); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8470,16 +9236,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8492,16 +9262,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8515,16 +9289,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8538,16 +9316,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8561,16 +9343,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8584,16 +9370,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8607,16 +9397,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8624,22 +9418,27 @@ abstract class GeckoStateEvents { final List args = message! as List; final int arg_sequence = args[0]! as int; final String arg_id = args[1]! as String; - final List arg_results = (args[2]! as List).cast(); + final List arg_results = (args[2]! as List) + .cast(); try { api.onFindResults(arg_sequence, arg_id, arg_results); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onLongPress$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onLongPress$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8653,16 +9452,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onPreferenceChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onPreferenceChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8675,38 +9478,73 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { final List args = message! as List; final int arg_sequence = args[0]! as int; - final ContainerSiteAssignment arg_details = args[1]! as ContainerSiteAssignment; + final ContainerSiteAssignment arg_details = + args[1]! as ContainerSiteAssignment; try { api.onContainerSiteAssignment(arg_sequence, arg_details); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onMlProgress$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onProxyLoadError$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final int arg_sequence = args[0]! as int; + final ProxyLoadError arg_details = args[1]! as ProxyLoadError; + try { + api.onProxyLoadError(arg_sequence, arg_details); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onMlProgress$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8719,16 +9557,20 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onManifestUpdate$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onManifestUpdate$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8742,52 +9584,64 @@ abstract class GeckoStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTranslationEngineStateChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTranslationEngineStateChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { final List args = message! as List; final int arg_sequence = args[0]! as int; - final TranslationEngineStateData arg_state = args[1]! as TranslationEngineStateData; + final TranslationEngineStateData arg_state = + args[1]! as TranslationEngineStateData; try { api.onTranslationEngineStateChange(arg_sequence, arg_state); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabTranslationStateChange$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabTranslationStateChange$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { final List args = message! as List; final int arg_sequence = args[0]! as int; - final TabTranslationStateData arg_state = args[1]! as TabTranslationStateData; + final TabTranslationStateData arg_state = + args[1]! as TabTranslationStateData; try { api.onTabTranslationStateChange(arg_sequence, arg_state); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -8806,12 +9660,20 @@ abstract class GeckoSyncStateEvents { void onSyncError(int sequence, String? errorMessage); - static void setUp(GeckoSyncStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + GeckoSyncStateEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncStateEvents.onAuthStateChanged$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncStateEvents.onAuthStateChanged$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8824,16 +9686,20 @@ abstract class GeckoSyncStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncStateEvents.onSyncStarted$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncStateEvents.onSyncStarted$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8845,16 +9711,20 @@ abstract class GeckoSyncStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncStateEvents.onSyncCompleted$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncStateEvents.onSyncCompleted$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8866,16 +9736,20 @@ abstract class GeckoSyncStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncStateEvents.onSyncError$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSyncStateEvents.onSyncError$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8888,8 +9762,10 @@ abstract class GeckoSyncStateEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -8902,12 +9778,20 @@ abstract class GeckoLogging { void onLog(LogLevel level, String message); - static void setUp(GeckoLogging? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + GeckoLogging? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoLogging.onLog$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoLogging.onLog$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -8920,8 +9804,10 @@ abstract class GeckoLogging { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -8933,9 +9819,13 @@ class ReaderViewEvents { /// Constructor for [ReaderViewEvents]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - ReaderViewEvents({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + ReaderViewEvents({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -8943,25 +9833,28 @@ class ReaderViewEvents { final String pigeonVar_messageChannelSuffix; Future onToggleReaderView(bool enable) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewEvents.onToggleReaderView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewEvents.onToggleReaderView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([enable]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [enable], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future onAppearanceButtonTap() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewEvents.onAppearanceButtonTap$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewEvents.onAppearanceButtonTap$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -8971,11 +9864,10 @@ class ReaderViewEvents { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -8984,12 +9876,20 @@ abstract class ReaderViewController { void appearanceButtonVisibility(int sequence, bool visible); - static void setUp(ReaderViewController? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + ReaderViewController? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -9002,8 +9902,10 @@ abstract class ReaderViewController { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -9015,9 +9917,13 @@ class GeckoSelectionActionController { /// Constructor for [GeckoSelectionActionController]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoSelectionActionController({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoSelectionActionController({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -9025,21 +9931,23 @@ class GeckoSelectionActionController { final String pigeonVar_messageChannelSuffix; Future setActions(List actions) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSelectionActionController.setActions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSelectionActionController.setActions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([actions]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [actions], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -9048,12 +9956,20 @@ abstract class GeckoSelectionActionEvents { void performSelectionAction(String id, String selectedText); - static void setUp(GeckoSelectionActionEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + GeckoSelectionActionEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSelectionActionEvents.performSelectionAction$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSelectionActionEvents.performSelectionAction$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -9066,8 +9982,10 @@ abstract class GeckoSelectionActionEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -9079,9 +9997,13 @@ class GeckoAddonsApi { /// Constructor for [GeckoAddonsApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoAddonsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoAddonsApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -9089,233 +10011,275 @@ class GeckoAddonsApi { final String pigeonVar_messageChannelSuffix; Future> getAddons(bool allowCache) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getAddons$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getAddons$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([allowCache]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [allowCache], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } Future getAddonById(String addonId, bool allowCache) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getAddonById$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getAddonById$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId, allowCache]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId, allowCache], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as AddonInfo?; } Future getAddonStoreInfo(String addonId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getAddonStoreInfo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getAddonStoreInfo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as AddonStoreInfo?; } - Future> searchAddonListings(String query, AddonStoreApp app, int page, int pageSize) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.searchAddonListings$pigeonVar_messageChannelSuffix'; + Future> searchAddonListings( + String query, + AddonStoreApp app, + int page, + int pageSize, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.searchAddonListings$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([query, app, page, pageSize]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [query, app, page, pageSize], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } - Future> getFeaturedAddonListings(AddonStoreApp app, int pageSize) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getFeaturedAddonListings$pigeonVar_messageChannelSuffix'; + Future> getFeaturedAddonListings( + AddonStoreApp app, + int pageSize, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getFeaturedAddonListings$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([app, pageSize]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [app, pageSize], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } - Future invokeAddonAction(String extensionId, WebExtensionActionType actionType) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$pigeonVar_messageChannelSuffix'; + Future invokeAddonAction( + String extensionId, + WebExtensionActionType actionType, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([extensionId, actionType]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [extensionId, actionType], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future enableAddon(String addonId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.enableAddon$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.enableAddon$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as AddonInfo; } Future disableAddon(String addonId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.disableAddon$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.disableAddon$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as AddonInfo; } - Future setAddonAllowedInPrivateBrowsing(String addonId, bool allowed) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.setAddonAllowedInPrivateBrowsing$pigeonVar_messageChannelSuffix'; + Future setAddonAllowedInPrivateBrowsing( + String addonId, + bool allowed, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.setAddonAllowedInPrivateBrowsing$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId, allowed]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId, allowed], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as AddonInfo; } - Future setAddonAutoUpdateEnabledForAddon(String addonId, bool enabled) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.setAddonAutoUpdateEnabledForAddon$pigeonVar_messageChannelSuffix'; + Future setAddonAutoUpdateEnabledForAddon( + String addonId, + bool enabled, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.setAddonAutoUpdateEnabledForAddon$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId, enabled]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId, enabled], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as AddonInfo; } Future uninstallAddon(String addonId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.uninstallAddon$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.uninstallAddon$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future triggerAddonUpdate(String addonId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.triggerAddonUpdate$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.triggerAddonUpdate$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as AddonUpdateAttemptInfo?; } Future triggerAllAddonUpdates() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.triggerAllAddonUpdates$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.triggerAllAddonUpdates$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -9325,52 +10289,58 @@ class GeckoAddonsApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future getLastAddonUpdateAttempt(String addonId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getLastAddonUpdateAttempt$pigeonVar_messageChannelSuffix'; + Future getLastAddonUpdateAttempt( + String addonId, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getLastAddonUpdateAttempt$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([addonId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [addonId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as AddonUpdateAttemptInfo?; } Future installAddon(String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.installAddon$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.installAddon$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future isAddonAutoUpdateEnabled() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.isAddonAutoUpdateEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.isAddonAutoUpdateEnabled$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -9380,52 +10350,75 @@ class GeckoAddonsApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } Future setAddonAutoUpdateEnabled(bool enabled) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.setAddonAutoUpdateEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.setAddonAutoUpdateEnabled$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [enabled], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } abstract class GeckoAddonEvents { static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - void onUpsertWebExtensionAction(int sequence, String extensionId, WebExtensionActionType actionType, WebExtensionData extensionData); + void onUpsertWebExtensionAction( + int sequence, + String extensionId, + WebExtensionActionType actionType, + WebExtensionData extensionData, + ); - void onRemoveWebExtensionAction(int sequence, String extensionId, WebExtensionActionType actionType); + void onRemoveWebExtensionAction( + int sequence, + String extensionId, + WebExtensionActionType actionType, + ); - void onUpdateWebExtensionIcon(int sequence, String extensionId, WebExtensionActionType actionType, Uint8List icon); + void onUpdateWebExtensionIcon( + int sequence, + String extensionId, + WebExtensionActionType actionType, + Uint8List icon, + ); void onWebExtensionPopupRequested(String extensionId, String extensionName); void onOpenAddonSettingsRequested(String addonId); - static void setUp(GeckoAddonEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + GeckoAddonEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -9433,23 +10426,34 @@ abstract class GeckoAddonEvents { final List args = message! as List; final int arg_sequence = args[0]! as int; final String arg_extensionId = args[1]! as String; - final WebExtensionActionType arg_actionType = args[2]! as WebExtensionActionType; - final WebExtensionData arg_extensionData = args[3]! as WebExtensionData; + final WebExtensionActionType arg_actionType = + args[2]! as WebExtensionActionType; + final WebExtensionData arg_extensionData = + args[3]! as WebExtensionData; try { - api.onUpsertWebExtensionAction(arg_sequence, arg_extensionId, arg_actionType, arg_extensionData); + api.onUpsertWebExtensionAction( + arg_sequence, + arg_extensionId, + arg_actionType, + arg_extensionData, + ); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -9457,22 +10461,31 @@ abstract class GeckoAddonEvents { final List args = message! as List; final int arg_sequence = args[0]! as int; final String arg_extensionId = args[1]! as String; - final WebExtensionActionType arg_actionType = args[2]! as WebExtensionActionType; + final WebExtensionActionType arg_actionType = + args[2]! as WebExtensionActionType; try { - api.onRemoveWebExtensionAction(arg_sequence, arg_extensionId, arg_actionType); + api.onRemoveWebExtensionAction( + arg_sequence, + arg_extensionId, + arg_actionType, + ); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -9480,23 +10493,33 @@ abstract class GeckoAddonEvents { final List args = message! as List; final int arg_sequence = args[0]! as int; final String arg_extensionId = args[1]! as String; - final WebExtensionActionType arg_actionType = args[2]! as WebExtensionActionType; + final WebExtensionActionType arg_actionType = + args[2]! as WebExtensionActionType; final Uint8List arg_icon = args[3]! as Uint8List; try { - api.onUpdateWebExtensionIcon(arg_sequence, arg_extensionId, arg_actionType, arg_icon); + api.onUpdateWebExtensionIcon( + arg_sequence, + arg_extensionId, + arg_actionType, + arg_icon, + ); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onWebExtensionPopupRequested$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onWebExtensionPopupRequested$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -9505,20 +10528,27 @@ abstract class GeckoAddonEvents { final String arg_extensionId = args[0]! as String; final String arg_extensionName = args[1]! as String; try { - api.onWebExtensionPopupRequested(arg_extensionId, arg_extensionName); + api.onWebExtensionPopupRequested( + arg_extensionId, + arg_extensionName, + ); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onOpenAddonSettingsRequested$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onOpenAddonSettingsRequested$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -9530,8 +10560,10 @@ abstract class GeckoAddonEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -9543,9 +10575,13 @@ class GeckoSuggestionApi { /// Constructor for [GeckoSuggestionApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoSuggestionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoSuggestionApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -9553,69 +10589,96 @@ class GeckoSuggestionApi { final String pigeonVar_messageChannelSuffix; Future getAutocompleteSuggestion(String query) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.getAutocompleteSuggestion$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.getAutocompleteSuggestion$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([query]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [query], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as AutocompleteResult?; } - Future querySuggestions(String text, List providers) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.querySuggestions$pigeonVar_messageChannelSuffix'; + Future querySuggestions( + String text, + List providers, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.querySuggestions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([text, providers]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [text, providers], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } abstract class GeckoSuggestionEvents { static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - void onSuggestionResult(int sequence, GeckoSuggestionType suggestionType, List suggestions); + void onSuggestionResult( + int sequence, + GeckoSuggestionType suggestionType, + List suggestions, + ); - static void setUp(GeckoSuggestionEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + GeckoSuggestionEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { final List args = message! as List; final int arg_sequence = args[0]! as int; - final GeckoSuggestionType arg_suggestionType = args[1]! as GeckoSuggestionType; - final List arg_suggestions = (args[2]! as List).cast(); + final GeckoSuggestionType arg_suggestionType = + args[1]! as GeckoSuggestionType; + final List arg_suggestions = + (args[2]! as List).cast(); try { - api.onSuggestionResult(arg_sequence, arg_suggestionType, arg_suggestions); + api.onSuggestionResult( + arg_sequence, + arg_suggestionType, + arg_suggestions, + ); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -9628,12 +10691,20 @@ abstract class GeckoTabContentEvents { void onContentUpdate(int sequence, TabContent content); - static void setUp(GeckoTabContentEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + GeckoTabContentEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -9646,8 +10717,10 @@ abstract class GeckoTabContentEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -9659,9 +10732,13 @@ class GeckoDeleteBrowsingDataController { /// Constructor for [GeckoDeleteBrowsingDataController]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoDeleteBrowsingDataController({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoDeleteBrowsingDataController({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -9669,7 +10746,8 @@ class GeckoDeleteBrowsingDataController { final String pigeonVar_messageChannelSuffix; Future deleteTabs() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteTabs$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteTabs$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -9679,15 +10757,15 @@ class GeckoDeleteBrowsingDataController { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future deleteBrowsingHistory() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteBrowsingHistory$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteBrowsingHistory$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -9697,15 +10775,15 @@ class GeckoDeleteBrowsingDataController { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future deleteCookiesAndSiteData() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCookiesAndSiteData$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCookiesAndSiteData$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -9715,15 +10793,15 @@ class GeckoDeleteBrowsingDataController { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future deleteCachedFiles() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCachedFiles$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCachedFiles$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -9733,15 +10811,15 @@ class GeckoDeleteBrowsingDataController { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future deleteSitePermissions() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteSitePermissions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteSitePermissions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -9751,15 +10829,15 @@ class GeckoDeleteBrowsingDataController { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future deleteDownloads() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteDownloads$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteDownloads$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -9769,48 +10847,54 @@ class GeckoDeleteBrowsingDataController { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future clearDataForSessionContext(String contextId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.clearDataForSessionContext$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.clearDataForSessionContext$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([contextId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [contextId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Clear browsing data for a specific host/domain - Future clearDataForHost(String host, List dataTypes) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.clearDataForHost$pigeonVar_messageChannelSuffix'; + Future clearDataForHost( + String host, + List dataTypes, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.clearDataForHost$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([host, dataTypes]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [host, dataTypes], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -9818,163 +10902,197 @@ class GeckoHistoryApi { /// Constructor for [GeckoHistoryApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoHistoryApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoHistoryApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; - Future> getDetailedVisits(int startMillis, int endMillis, List excludeTypes) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getDetailedVisits$pigeonVar_messageChannelSuffix'; + Future> getDetailedVisits( + int startMillis, + int endMillis, + List excludeTypes, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getDetailedVisits$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([startMillis, endMillis, excludeTypes]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [startMillis, endMillis, excludeTypes], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } - Future> getVisitsPaginated(int offset, int count, List excludeTypes) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getVisitsPaginated$pigeonVar_messageChannelSuffix'; + Future> getVisitsPaginated( + int offset, + int count, + List excludeTypes, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getVisitsPaginated$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([offset, count, excludeTypes]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [offset, count, excludeTypes], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } Future deleteVisit(String url, int timestamp) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisit$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisit$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, timestamp]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url, timestamp], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future deleteDownload(String id) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteDownload$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteDownload$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([id]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [id], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } Future deleteVisitsBetween(int startMillis, int endMillis) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsBetween$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsBetween$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([startMillis, endMillis]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [startMillis, endMillis], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future> getHistoryHighlights(HistoryHighlightWeights weights, int limit) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getHistoryHighlights$pigeonVar_messageChannelSuffix'; + Future> getHistoryHighlights( + HistoryHighlightWeights weights, + int limit, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getHistoryHighlights$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([weights, limit]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [weights, limit], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } - Future> getTopFrecentSites(int limit, FrecencyThresholdOption frecencyThreshold) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getTopFrecentSites$pigeonVar_messageChannelSuffix'; + Future> getTopFrecentSites( + int limit, + FrecencyThresholdOption frecencyThreshold, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getTopFrecentSites$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([limit, frecencyThreshold]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [limit, frecencyThreshold], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } /// Returns the most recent [HistoryMetadata] record for [url], or `null` if /// no metadata has been recorded for that URL. Future getLatestHistoryMetadataForUrl(String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getLatestHistoryMetadataForUrl$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getLatestHistoryMetadataForUrl$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as HistoryMetadata?; } @@ -9982,191 +11100,227 @@ class GeckoHistoryApi { /// input URL aligned by index; entries are `null` for URLs Places has no /// metadata for. Used by the local search re-rank to collapse N IPC /// roundtrips into one. - Future> getLatestHistoryMetadataForUrls(List urls) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getLatestHistoryMetadataForUrls$pigeonVar_messageChannelSuffix'; + Future> getLatestHistoryMetadataForUrls( + List urls, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getLatestHistoryMetadataForUrls$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([urls]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [urls], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } /// Bulk visited check: returns booleans aligned with [urls] indicating /// whether Places has any visit recorded for each URL. Future> getVisited(List urls) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getVisited$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getVisited$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([urls]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [urls], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } /// Frecency-ranked autocomplete results. Mirrors Places' awesomebar input. - Future> getSuggestions(String query, int limit) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getSuggestions$pigeonVar_messageChannelSuffix'; + Future> getSuggestions( + String query, + int limit, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.getSuggestions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([query, limit]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [query, limit], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } /// Places' built-in metadata text search (matches title / url / searchTerm). /// Useful as a comparison baseline against the local content FTS. - Future> queryHistoryMetadata(String query, int limit) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.queryHistoryMetadata$pigeonVar_messageChannelSuffix'; + Future> queryHistoryMetadata( + String query, + int limit, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.queryHistoryMetadata$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([query, limit]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [query, limit], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } /// Records a title / preview-image observation for [url] without recording /// a visit. Intended for manual flows; the engine middleware records these /// automatically as the user browses. - Future recordObservation(String url, PageObservation observation) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.recordObservation$pigeonVar_messageChannelSuffix'; + Future recordObservation( + String url, + PageObservation observation, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.recordObservation$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, observation]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url, observation], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Records a view-time observation against the metadata record identified /// by [key]. View time is added to the existing total. - Future noteHistoryMetadataViewTime(HistoryMetadataKey key, int viewTimeMs) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.noteHistoryMetadataViewTime$pigeonVar_messageChannelSuffix'; + Future noteHistoryMetadataViewTime( + HistoryMetadataKey key, + int viewTimeMs, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.noteHistoryMetadataViewTime$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([key, viewTimeMs]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [key, viewTimeMs], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Records a document-type observation against the metadata record /// identified by [key]. - Future noteHistoryMetadataDocumentType(HistoryMetadataKey key, DocumentType documentType) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.noteHistoryMetadataDocumentType$pigeonVar_messageChannelSuffix'; + Future noteHistoryMetadataDocumentType( + HistoryMetadataKey key, + DocumentType documentType, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.noteHistoryMetadataDocumentType$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([key, documentType]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [key, documentType], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Removes all visits for [url]. May propagate to remote devices via Sync. Future deleteVisitsFor(String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsFor$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsFor$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Removes all visits since [sinceMillis] (inclusive). May propagate to /// remote devices via Sync. Future deleteVisitsSince(int sinceMillis) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsSince$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsSince$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([sinceMillis]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [sinceMillis], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Removes all locally stored history. Sync will not remove remote history, /// but it will prevent deleted entries from returning. Future deleteEverything() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteEverything$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteEverything$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -10176,30 +11330,31 @@ class GeckoHistoryApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Prunes history metadata older than [olderThanMillis] (exclusive). Future deleteHistoryMetadataOlderThan(int olderThanMillis) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteHistoryMetadataOlderThan$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteHistoryMetadataOlderThan$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([olderThanMillis]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [olderThanMillis], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -10207,9 +11362,13 @@ class GeckoDownloadsApi { /// Constructor for [GeckoDownloadsApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoDownloadsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoDownloadsApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -10217,57 +11376,69 @@ class GeckoDownloadsApi { final String pigeonVar_messageChannelSuffix; Future requestDownload(String tabId, DownloadState state) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDownloadsApi.requestDownload$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDownloadsApi.requestDownload$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, state]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, state], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future copyInternetResource(String tabId, ShareInternetResourceState state) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDownloadsApi.copyInternetResource$pigeonVar_messageChannelSuffix'; + Future copyInternetResource( + String tabId, + ShareInternetResourceState state, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDownloadsApi.copyInternetResource$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, state]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, state], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } - Future shareInternetResource(String tabId, ShareInternetResourceState state) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDownloadsApi.shareInternetResource$pigeonVar_messageChannelSuffix'; + Future shareInternetResource( + String tabId, + ShareInternetResourceState state, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDownloadsApi.shareInternetResource$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, state]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, state], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -10276,12 +11447,20 @@ abstract class BrowserExtensionEvents { void onFeedRequested(int sequence, String url); - static void setUp(BrowserExtensionEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + BrowserExtensionEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.BrowserExtensionEvents.onFeedRequested$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.BrowserExtensionEvents.onFeedRequested$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -10294,8 +11473,10 @@ abstract class BrowserExtensionEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -10307,9 +11488,13 @@ class GeckoFetchApi { /// Constructor for [GeckoFetchApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoFetchApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoFetchApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -10317,21 +11502,23 @@ class GeckoFetchApi { final String pigeonVar_messageChannelSuffix; Future fetch(GeckoFetchRequest request) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFetchApi.fetch$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFetchApi.fetch$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([request]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [request], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as GeckoFetchResponse; } } @@ -10349,9 +11536,13 @@ class GeckoViewportApi { /// Constructor for [GeckoViewportApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoViewportApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoViewportApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -10368,21 +11559,23 @@ class GeckoViewportApi { /// /// [heightPx] Combined height of top and bottom toolbars in pixels. Future setDynamicToolbarMaxHeight(int heightPx) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setDynamicToolbarMaxHeight$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setDynamicToolbarMaxHeight$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([heightPx]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [heightPx], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Sets the vertical clipping offset for the GeckoView content. @@ -10395,21 +11588,23 @@ class GeckoViewportApi { /// /// [clippingPx] The clipping offset in pixels. Negative = bottom clip. Future setVerticalClipping(int clippingPx) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setVerticalClipping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setVerticalClipping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([clippingPx]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [clippingPx], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -10429,7 +11624,12 @@ abstract class GeckoViewportEvents { /// [heightPx] Keyboard height in pixels (0 when hidden). /// [isVisible] Whether the keyboard is currently visible. /// [isAnimating] Whether the keyboard is currently animating. - void onKeyboardVisibilityChanged(int sequence, int heightPx, bool isVisible, bool isAnimating); + void onKeyboardVisibilityChanged( + int sequence, + int heightPx, + bool isVisible, + bool isAnimating, + ); /// Called when GeckoView scroll-handling eligibility changes. /// @@ -10439,12 +11639,20 @@ abstract class GeckoViewportEvents { /// the page consumed touch input. void onBrowserHandlingScrollChanged(int sequence, bool isHandling); - static void setUp(GeckoViewportEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + GeckoViewportEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -10455,20 +11663,29 @@ abstract class GeckoViewportEvents { final bool arg_isVisible = args[2]! as bool; final bool arg_isAnimating = args[3]! as bool; try { - api.onKeyboardVisibilityChanged(arg_sequence, arg_heightPx, arg_isVisible, arg_isAnimating); + api.onKeyboardVisibilityChanged( + arg_sequence, + arg_heightPx, + arg_isVisible, + arg_isAnimating, + ); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -10481,8 +11698,10 @@ abstract class GeckoViewportEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -10494,9 +11713,13 @@ class GeckoBookmarksApi { /// Constructor for [GeckoBookmarksApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoBookmarksApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoBookmarksApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -10509,21 +11732,23 @@ class GeckoBookmarksApi { /// @param recursive Whether to recurse and obtain all levels of children. /// @return The populated root starting from the guid. Future getTree(String guid, bool recursive) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getTree$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getTree$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([guid, recursive]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [guid, recursive], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as BookmarkNode?; } @@ -10532,21 +11757,23 @@ class GeckoBookmarksApi { /// @param guid The bookmark guid to obtain. /// @return The bookmark node or null if it does not exist. Future getBookmark(String guid) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getBookmark$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getBookmark$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([guid]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [guid], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as BookmarkNode?; } @@ -10555,21 +11782,23 @@ class GeckoBookmarksApi { /// @param url The URL string. /// @return The list of bookmarks that match the URL Future> getBookmarksWithUrl(String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getBookmarksWithUrl$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getBookmarksWithUrl$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } @@ -10579,22 +11808,28 @@ class GeckoBookmarksApi { /// @param maxAge Optional parameter used to filter out entries older than this number of milliseconds. /// @param currentTime Optional parameter for current time. Defaults toSystem.currentTimeMillis() /// @return The list of bookmarks that have been recently added up to the limit number of items. - Future> getRecentBookmarks(int limit, int? maxAge, int currentTime) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getRecentBookmarks$pigeonVar_messageChannelSuffix'; + Future> getRecentBookmarks( + int limit, + int? maxAge, + int currentTime, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getRecentBookmarks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([limit, maxAge, currentTime]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [limit, maxAge, currentTime], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } @@ -10604,21 +11839,23 @@ class GeckoBookmarksApi { /// @param limit The maximum number of entries to return. /// @return The list of matching bookmark nodes up to the limit number of items. Future> searchBookmarks(String query, int limit) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.searchBookmarks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.searchBookmarks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([query, limit]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [query, limit], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } @@ -10631,22 +11868,29 @@ class GeckoBookmarksApi { /// @param title The title of the bookmark item to add. /// @param position The optional position to add the new node or null to append. /// @return The guid of the newly inserted bookmark item. - Future addItem(String parentGuid, String url, String title, int? position) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.addItem$pigeonVar_messageChannelSuffix'; + Future addItem( + String parentGuid, + String url, + String title, + int? position, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.addItem$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([parentGuid, url, title, position]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [parentGuid, url, title, position], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } @@ -10658,22 +11902,28 @@ class GeckoBookmarksApi { /// @param title The title of the bookmark folder to add. /// @param position The optional position to add the new node or null to append. /// @return The guid of the newly inserted bookmark item. - Future addFolder(String parentGuid, String title, int? position) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.addFolder$pigeonVar_messageChannelSuffix'; + Future addFolder( + String parentGuid, + String title, + int? position, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.addFolder$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([parentGuid, title, position]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [parentGuid, title, position], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } @@ -10684,21 +11934,23 @@ class GeckoBookmarksApi { /// @param guid The guid of the item to update. /// @param info The info to change in the bookmark. Future updateNode(String guid, BookmarkInfo info) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.updateNode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.updateNode$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([guid, info]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [guid, info], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Deletes a bookmark node and all of its children, if any. @@ -10707,21 +11959,23 @@ class GeckoBookmarksApi { /// /// @return Whether the bookmark existed or not. Future deleteNode(String guid) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.deleteNode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.deleteNode$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([guid]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [guid], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } } @@ -10731,9 +11985,13 @@ class GeckoSitePermissionsApi { /// Constructor for [GeckoSitePermissionsApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoSitePermissionsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoSitePermissionsApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -10741,61 +11999,73 @@ class GeckoSitePermissionsApi { final String pigeonVar_messageChannelSuffix; /// Get permissions for origin (single source of truth from GeckoView) - Future getSitePermissions(String origin, bool private) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.getSitePermissions$pigeonVar_messageChannelSuffix'; + Future getSitePermissions( + String origin, + bool private, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.getSitePermissions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([origin, private]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [origin, private], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as SitePermissions?; } /// Save/update permissions (persisted by GeckoView) - Future setSitePermissions(SitePermissions permissions, bool private) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.setSitePermissions$pigeonVar_messageChannelSuffix'; + Future setSitePermissions( + SitePermissions permissions, + bool private, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.setSitePermissions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([permissions, private]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [permissions, private], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Delete permissions for origin (removed from GeckoView storage) Future deleteSitePermissions(String origin, bool private) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.deleteSitePermissions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.deleteSitePermissions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([origin, private]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [origin, private], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -10804,9 +12074,13 @@ class GeckoPublicSuffixListApi { /// Constructor for [GeckoPublicSuffixListApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoPublicSuffixListApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoPublicSuffixListApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -10816,21 +12090,23 @@ class GeckoPublicSuffixListApi { /// Get base domain (eTLD+1) from host using Mozilla's Public Suffix List /// Returns the host unchanged if PSL lookup fails Future getPublicSuffixPlusOne(String host) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPublicSuffixListApi.getPublicSuffixPlusOne$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPublicSuffixListApi.getPublicSuffixPlusOne$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([host]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [host], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as String; } } @@ -10844,9 +12120,13 @@ class GeckoTrackingProtectionApi { /// Constructor for [GeckoTrackingProtectionApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoTrackingProtectionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoTrackingProtectionApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -10858,21 +12138,23 @@ class GeckoTrackingProtectionApi { /// Uses callback pattern to match Mozilla Android Components API. /// Returns true if the site is in the exceptions list (ETP disabled). Future containsException(String tabId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.containsException$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.containsException$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } @@ -10881,21 +12163,23 @@ class GeckoTrackingProtectionApi { /// This adds the current tab's URL to the exceptions list. /// ETP will be disabled for this site until the exception is removed. Future addException(String tabId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.addException$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.addException$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Remove tracking protection exception for a tab (enable ETP for this site) @@ -10903,21 +12187,23 @@ class GeckoTrackingProtectionApi { /// This removes the current tab's URL from the exceptions list. /// ETP will be re-enabled for this site. Future removeException(String tabId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeException$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeException$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Remove a specific exception by URL @@ -10925,28 +12211,31 @@ class GeckoTrackingProtectionApi { /// Alternative to removeException(tabId) for cases where you /// have a URL rather than a tabId. Future removeExceptionByUrl(String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeExceptionByUrl$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeExceptionByUrl$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Fetch all tracking protection exceptions /// /// Returns list of all sites that have exceptions (ETP disabled). Future> fetchExceptions() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.fetchExceptions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.fetchExceptions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -10956,19 +12245,20 @@ class GeckoTrackingProtectionApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; - return (pigeonVar_replyValue! as List).cast(); + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as List) + .cast(); } /// Remove all tracking protection exceptions /// /// This re-enables ETP for all exception sites. Future removeAllExceptions() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeAllExceptions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeAllExceptions$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -10978,11 +12268,10 @@ class GeckoTrackingProtectionApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -10994,9 +12283,13 @@ class GeckoAppLinksApi { /// Constructor for [GeckoAppLinksApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoAppLinksApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoAppLinksApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -11010,21 +12303,23 @@ class GeckoAppLinksApi { /// /// Returns true if an external app is available, false otherwise. Future hasExternalApp(String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAppLinksApi.hasExternalApp$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAppLinksApi.hasExternalApp$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } @@ -11037,21 +12332,23 @@ class GeckoAppLinksApi { /// /// Returns true if URL was opened in external app, false if no app available. Future openAppLink(String url) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAppLinksApi.openAppLink$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAppLinksApi.openAppLink$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } } @@ -11064,9 +12361,13 @@ class GeckoPwaApi { /// Constructor for [GeckoPwaApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GeckoPwaApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + GeckoPwaApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -11084,28 +12385,36 @@ class GeckoPwaApi { /// The [contextId] is the container's contextual identity (optional, null for default container). /// The [overrideAppName] customizes the installed app's displayed name and persists in the saved manifest. /// Returns true if installation was successful. - Future installWebApp(String? tabId, String profileUuid, String? contextId, String? overrideAppName) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPwaApi.installWebApp$pigeonVar_messageChannelSuffix'; + Future installWebApp( + String? tabId, + String profileUuid, + String? contextId, + String? overrideAppName, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPwaApi.installWebApp$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, profileUuid, contextId, overrideAppName]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, profileUuid, contextId, overrideAppName], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } /// Returns a list of all installed PWA manifests. Future> getInstalledWebApps() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPwaApi.getInstalledWebApps$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPwaApi.getInstalledWebApps$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -11115,11 +12424,10 @@ class GeckoPwaApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } @@ -11134,22 +12442,29 @@ class GeckoPwaApi { /// The [contextId] is the container's contextual identity (optional). /// The [overrideShortcutName] allows customizing the shortcut label. /// Returns true if the shortcut was created successfully. - Future installBasicShortcut(String? tabId, String profileUuid, String? contextId, String? overrideShortcutName) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPwaApi.installBasicShortcut$pigeonVar_messageChannelSuffix'; + Future installBasicShortcut( + String? tabId, + String profileUuid, + String? contextId, + String? overrideShortcutName, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPwaApi.installBasicShortcut$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId, profileUuid, contextId, overrideShortcutName]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId, profileUuid, contextId, overrideShortcutName], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } } @@ -11160,9 +12475,13 @@ class SandboxCaptureApi { /// Constructor for [SandboxCaptureApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - SandboxCaptureApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + SandboxCaptureApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -11173,59 +12492,65 @@ class SandboxCaptureApi { /// Dart has brought up [CaptureServer] and reconciled local artifacts with /// the `capture_tab` rows. Future resetAll(List entries) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureApi.resetAll$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureApi.resetAll$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([entries]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [entries], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Inserts or updates the registry entry for [entry.tabId]. Future mark(SandboxCaptureEntry entry) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureApi.mark$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureApi.mark$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([entry]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [entry], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Removes the registry entry for [tabId]. Future unmark(String tabId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureApi.unmark$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureApi.unmark$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [tabId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -11245,14 +12570,27 @@ abstract class SandboxCaptureHostEvents { /// The native middleware has already rewritten the new tab's URL to /// `about:blank`; Dart should register it as sandbox and run the capture /// pipeline for [targetUrl]. - void onSandboxNewTab(int sequence, String parentTabId, String newTabId, String targetUrl); + void onSandboxNewTab( + int sequence, + String parentTabId, + String newTabId, + String targetUrl, + ); - static void setUp(SandboxCaptureHostEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + SandboxCaptureHostEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureHostEvents.onSandboxLinkClick$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureHostEvents.onSandboxLinkClick$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -11262,20 +12600,28 @@ abstract class SandboxCaptureHostEvents { final String arg_parentTabId = args[1]! as String; final String arg_targetUrl = args[2]! as String; try { - api.onSandboxLinkClick(arg_sequence, arg_parentTabId, arg_targetUrl); + api.onSandboxLinkClick( + arg_sequence, + arg_parentTabId, + arg_targetUrl, + ); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureHostEvents.onSandboxNewTab$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_mozilla_components.SandboxCaptureHostEvents.onSandboxNewTab$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -11286,12 +12632,19 @@ abstract class SandboxCaptureHostEvents { final String arg_newTabId = args[2]! as String; final String arg_targetUrl = args[3]! as String; try { - api.onSandboxNewTab(arg_sequence, arg_parentTabId, arg_newTabId, arg_targetUrl); + api.onSandboxNewTab( + arg_sequence, + arg_parentTabId, + arg_newTabId, + arg_targetUrl, + ); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index c550f8f5..3fad0a67 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1856,11 +1856,40 @@ abstract class GeckoBrowserExtensionApi { List getMarkdown(List htmlList); } +class GeckoProxySettings { + final String id; + final String title; + final String type; + final String host; + final int port; + final String? username; + final String? password; + final bool proxyDNS; + final bool doNotProxyLocal; + + const GeckoProxySettings({ + required this.id, + required this.title, + required this.type, + required this.host, + required this.port, + this.username, + this.password, + this.proxyDNS = true, + this.doNotProxyLocal = true, + }); +} + @HostApi() abstract class GeckoContainerProxyApi { void setProxyPort(int port); void addContainerProxy(String contextId); void removeContainerProxy(String contextId); + void upsertProxy(GeckoProxySettings proxy); + void removeProxy(String proxyId); + void setContainerProxy(String contextId, String proxyId); + void clearContainerProxy(String contextId); + void removeContainerProxyRelation(String contextId, String proxyId); void setSiteAssignments(Map assignments); @async @@ -1930,6 +1959,20 @@ class ContainerSiteAssignment { }); } +class ProxyLoadError { + final String? tabId; + final String? contextId; + final String? url; + final String errorType; + + ProxyLoadError({ + required this.tabId, + required this.contextId, + required this.url, + required this.errorType, + }); +} + @FlutterApi() abstract class GeckoStateEvents { void onViewReadyStateChange(int sequence, bool state); @@ -1960,6 +2003,8 @@ abstract class GeckoStateEvents { void onContainerSiteAssignment(int sequence, ContainerSiteAssignment details); + void onProxyLoadError(int sequence, ProxyLoadError details); + void onMlProgress(int sequence, MlProgressData progress); void onManifestUpdate(int sequence, String tabId, PwaManifest? manifest); diff --git a/packages/flutter_singbox_proxy/.gitignore b/packages/flutter_singbox_proxy/.gitignore new file mode 100644 index 00000000..53be6d0b --- /dev/null +++ b/packages/flutter_singbox_proxy/.gitignore @@ -0,0 +1,37 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ + +# Built from official sing-box source by scripts/build-libbox-android.sh. +/android/libs/*.aar +!/android/libs/.gitkeep diff --git a/packages/flutter_singbox_proxy/.metadata b/packages/flutter_singbox_proxy/.metadata new file mode 100644 index 00000000..1b5e7ee3 --- /dev/null +++ b/packages/flutter_singbox_proxy/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + - platform: android + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/flutter_singbox_proxy/CHANGELOG.md b/packages/flutter_singbox_proxy/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/flutter_singbox_proxy/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_singbox_proxy/LICENSE b/packages/flutter_singbox_proxy/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/packages/flutter_singbox_proxy/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_singbox_proxy/README.md b/packages/flutter_singbox_proxy/README.md new file mode 100644 index 00000000..182bbf98 --- /dev/null +++ b/packages/flutter_singbox_proxy/README.md @@ -0,0 +1,59 @@ +# flutter_singbox_proxy + +Android sing-box proxy runtime plugin for WebLibre. + +The public API is Pigeon-based and intentionally uses a generic JSON profile +boundary. Flutter can add strongly typed editors over time while the native API +stays stable across sing-box protocol/schema updates. + +Current state: + +- Builds sing-box JSON for multiple simultaneous profile outbounds. +- Creates one authenticated local SOCKS inbound per active profile. +- Returns runtime endpoint metadata for Gecko container proxy integration. +- Exposes status/log callbacks. +- Starts libbox via reflection against the `io.nekohasekai.libbox.*` classes + shipped in the AAR — if the AAR is missing, the plugin still compiles but + `start` reports an `IllegalStateException` at runtime. + +## Building official sing-box libbox + +The plugin does not commit binary AARs. Build them from the official sing-box +checkout before release/F-Droid builds: + +```sh +packages/flutter_singbox_proxy/scripts/build-libbox-android.sh \ + --source /path/to/sing-box +``` + +By default the script looks for `../../../sing-box` relative to this package, +which matches the local workspace layout used during development. It copies +`libbox.aar` into `android/libs/`; Gradle consumes that AAR automatically when +present. + +For F-Droid metadata, call the package-local prebuild script: + +```sh +packages/flutter_singbox_proxy/scripts/fdroid-prebuild.sh \ + --source /path/to/sing-box +``` + +The build requires OpenJDK 17, Go, Android SDK/NDK, and gomobile/gobind. If +`JAVA_HOME` is unset and `/usr/lib/jvm/java-17-openjdk` exists, the script uses +that automatically. If gomobile/gobind are missing, the script installs the +official SagerNet gomobile tools with `go install`. + +### Version compatibility + +The script pins `github.com/sagernet/gomobile/cmd/gomobile@v0.1.12` and +`gobind@v0.1.12`. The sing-box source itself is whatever you point `--source` +at; the runtime accesses `io.nekohasekai.libbox.*` via reflection, so API drift +across sing-box releases surfaces as a `NoSuchMethodException` at start time +rather than a build failure. When bumping sing-box: + +1. Rebuild the AAR with the new tag. +2. Smoke-test `start`/`stop` on a profile to catch missing/renamed setup + methods (notably `SetupOptions.setOomKillerDisabled` / + `setOomKillerEnabled` — `LibboxRuntime` falls back between the two). +3. Update this README with the verified sing-box tag if relying on it for a + release. diff --git a/packages/flutter_singbox_proxy/analysis_options.yaml b/packages/flutter_singbox_proxy/analysis_options.yaml new file mode 100644 index 00000000..d059492c --- /dev/null +++ b/packages/flutter_singbox_proxy/analysis_options.yaml @@ -0,0 +1,4 @@ +# Analysis options for flutter_singbox_proxy package +# Includes root configuration from monorepo + +include: ../../analysis_options.yaml diff --git a/packages/flutter_singbox_proxy/android/.gitignore b/packages/flutter_singbox_proxy/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/flutter_singbox_proxy/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/flutter_singbox_proxy/android/build.gradle b/packages/flutter_singbox_proxy/android/build.gradle new file mode 100644 index 00000000..3a071df9 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/build.gradle @@ -0,0 +1,81 @@ +group = "eu.weblibre.flutter_singbox_proxy" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.3.21" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.13.2") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +def weblibreGoMobileAar = file("../../../native/go_mobile_runtime/build/weblibre-go.aar") +def requireWebLibreGoMobileAar = { + if (!weblibreGoMobileAar.exists()) { + throw new GradleException( + "Missing combined gomobile runtime AAR: ${weblibreGoMobileAar}. " + + "Run native/go_mobile_runtime/scripts/build-android.sh from the repository root." + ) + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "eu.weblibre.flutter_singbox_proxy" + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 24 + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen { false } + showStandardStreams = true + } + } + } +} + +dependencies { + requireWebLibreGoMobileAar() + compileOnly(files(weblibreGoMobileAar)) + + implementation("com.squareup.okhttp3:okhttp:4.12.0") + + testImplementation("org.json:json:20250517") + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.23.0") +} diff --git a/packages/flutter_singbox_proxy/android/libs/.gitkeep b/packages/flutter_singbox_proxy/android/libs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/flutter_singbox_proxy/android/settings.gradle b/packages/flutter_singbox_proxy/android/settings.gradle new file mode 100644 index 00000000..c42f7942 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_singbox_proxy' diff --git a/packages/flutter_singbox_proxy/android/src/main/AndroidManifest.xml b/packages/flutter_singbox_proxy/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..909ae150 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/FlutterSingboxProxyPlugin.kt b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/FlutterSingboxProxyPlugin.kt new file mode 100644 index 00000000..5967146b --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/FlutterSingboxProxyPlugin.kt @@ -0,0 +1,33 @@ +package eu.weblibre.flutter_singbox_proxy + +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyApi +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyEventsApi +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyLogMessage +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeState +import io.flutter.embedding.engine.plugins.FlutterPlugin + +/** Flutter plugin entry point for the sing-box proxy runtime. */ +class FlutterSingboxProxyPlugin : FlutterPlugin { + private var runtimeManager: SingboxRuntimeManager? = null + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + val eventsApi = SingboxProxyEventsApi(binding.binaryMessenger) + val manager = SingboxRuntimeManager( + context = binding.applicationContext, + onStateChanged = { state: SingboxProxyRuntimeState -> + eventsApi.onStateChanged(state) { } + }, + onLogMessage = { message: SingboxProxyLogMessage -> + eventsApi.onLogMessage(message) { } + } + ) + runtimeManager = manager + SingboxProxyApi.setUp(binding.binaryMessenger, manager) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + SingboxProxyApi.setUp(binding.binaryMessenger, null) + runtimeManager?.close() + runtimeManager = null + } +} diff --git a/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/LibboxRuntime.kt b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/LibboxRuntime.kt new file mode 100644 index 00000000..a97b02f2 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/LibboxRuntime.kt @@ -0,0 +1,485 @@ +package eu.weblibre.flutter_singbox_proxy + +import android.content.Context +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy + +open class LibboxRuntime( + private val context: Context, + private val dohResolver: PlatformDohResolver = PlatformDohResolver(), +) { + private var setupComplete = false + private var commandServer: Any? = null + private var commandServerStarted = false + private var commandClient: Any? = null + private var logSink: ((Int, String) -> Unit)? = null + private var logClientThread: Thread? = null + + // Read by the LocalDNSTransport proxy on libbox worker threads, written + // from the platform thread via setBootstrapDohUrl(). Volatile so the + // bridge sees the URL configured by the most recent start() call. + @Volatile + private var bootstrapDohUrl: String? = null + + /** + * Set the DoH endpoint the platform LocalDNSTransport will use for + * bootstrap lookups. Pass null to disable the bridge (sing-box's broken + * /etc/resolv.conf path will run, which is rarely what you want on + * Android). + */ + open fun setBootstrapDohUrl(url: String?) { + bootstrapDohUrl = url?.takeIf { it.isNotBlank() } + } + + open fun isAvailable(): Boolean = runCatching { + Class.forName(LIBBOX_CLASS) + }.isSuccess + + /** + * Register a callback that receives every log message emitted by sing-box + * (level: int, message: String). Pass null to clear. The callback is + * invoked from a background thread; the receiver must be thread-safe. + */ + @Synchronized + open fun setLogSink(sink: ((Int, String) -> Unit)?) { + logSink = sink + if (sink == null) { + disconnectLogClient() + } else if (commandServer != null) { + ensureLogClientConnected() + } + } + + @Synchronized + open fun start(configJson: String) { + ensureSetup() + val server = commandServer ?: newCommandServer().also { commandServer = it } + ensureCommandServerStarted(server) + val overrideOptions = newInstance(OVERRIDE_OPTIONS_CLASS) + invoke(overrideOptions, "setAutoRedirect", false) + invoke(overrideOptions, "setIncludePackage", emptyStringIterator()) + invoke(overrideOptions, "setExcludePackage", emptyStringIterator()) + invoke(server, "startOrReloadService", configJson, overrideOptions) + if (logSink != null) { + ensureLogClientConnected() + } + } + + @Synchronized + open fun stopService() { + commandServer?.let { server -> + runCatching { invoke(server, "closeService") } + } + disconnectLogClient() + } + + @Synchronized + open fun close() { + disconnectLogClient() + commandServer?.let { server -> + runCatching { invoke(server, "close") } + } + commandServer = null + commandServerStarted = false + } + + private fun ensureSetup() { + if (setupComplete) return + val baseDir = context.filesDir.resolve("singbox_proxy") + val workingDir = baseDir.resolve("working") + val tempDir = baseDir.resolve("tmp") + workingDir.mkdirs() + tempDir.mkdirs() + + val options = newInstance(SETUP_OPTIONS_CLASS) + invoke(options, "setBasePath", baseDir.absolutePath) + invoke(options, "setWorkingPath", workingDir.absolutePath) + invoke(options, "setTempPath", tempDir.absolutePath) + invoke(options, "setFixAndroidStack", true) + invoke(options, "setCommandServerListenPort", 0) + invoke(options, "setCommandServerSecret", "") + invoke(options, "setLogMaxLines", 300L) + invoke(options, "setDebug", false) + invokeIfAvailable(options, "setCrashReportSource", "flutter_singbox_proxy") + // sing-box renamed the OOM killer toggle between releases; only one of + // these exists on the linked libbox AAR, so call whichever responds. + invokeFirstAvailable( + options, + methodNames = listOf("setOomKillerDisabled", "setOomKillerEnabled"), + args = arrayOf(true), + ) + invokeIfAvailable(options, "setOomMemoryLimit", 0L) + + val libbox = Class.forName(LIBBOX_CLASS) + libbox.getMethod("setup", Class.forName(SETUP_OPTIONS_CLASS)).invoke(null, options) + setupComplete = true + } + + private fun newCommandServer(): Any { + val handlerInterface = Class.forName(COMMAND_SERVER_HANDLER_CLASS) + val platformInterface = Class.forName(PLATFORM_INTERFACE_CLASS) + val handler = Proxy.newProxyInstance( + handlerInterface.classLoader, + arrayOf(handlerInterface), + commandServerHandler() + ) + val platform = Proxy.newProxyInstance( + platformInterface.classLoader, + arrayOf(platformInterface), + platformHandler() + ) + return Class.forName(COMMAND_SERVER_CLASS) + .getConstructor(handlerInterface, platformInterface) + .newInstance(handler, platform) + } + + private fun ensureCommandServerStarted(server: Any) { + if (commandServerStarted) return + invoke(server, "start") + commandServerStarted = true + } + + private fun commandServerHandler(): InvocationHandler { + return InvocationHandler { _, method, args -> + when (method.name) { + "getSystemProxyStatus" -> newInstance(SYSTEM_PROXY_STATUS_CLASS).also { status -> + invoke(status, "setAvailable", false) + invoke(status, "setEnabled", false) + } + "serviceReload", "serviceStop", "setSystemProxyEnabled", "writeDebugMessage" -> null + "triggerNativeCrash" -> throw UnsupportedOperationException("Native crash trigger is disabled") + else -> defaultValue(method.returnType, args) + } + } + } + + private fun platformHandler(): InvocationHandler { + return InvocationHandler { _, method, args -> + when (method.name) { + "autoDetectInterfaceControl", + "clearDNSCache", + "closeDefaultInterfaceMonitor", + "closeNeighborMonitor", + "registerMyInterface", + "sendNotification", + "startDefaultInterfaceMonitor", + "startNeighborMonitor" -> null + "findConnectionOwner" -> newInstance(CONNECTION_OWNER_CLASS).also { owner -> + invoke(owner, "setUserId", -1) + invoke(owner, "setUserName", "") + invoke(owner, "setProcessPath", "") + invoke(owner, "setAndroidPackageNames", emptyStringIterator()) + } + "getInterfaces" -> emptyIterator(NETWORK_INTERFACE_ITERATOR_CLASS) + "includeAllNetworks", + "underNetworkExtension", + "usePlatformAutoDetectInterfaceControl", + "useProcFS" -> false + "localDNSTransport" -> createLocalDnsTransport() + "openTun" -> throw UnsupportedOperationException("TUN is not supported by WebLibre proxy routing") + "readWIFIState" -> Class.forName(WIFI_STATE_CLASS) + .getConstructor(String::class.java, String::class.java) + .newInstance("", "") + "systemCertificates" -> emptyStringIterator() + else -> defaultValue(method.returnType, args) + } + } + } + + private fun ensureLogClientConnected() { + if (commandClient != null) return + val handlerInterface = runCatching { + Class.forName(COMMAND_CLIENT_HANDLER_CLASS) + }.getOrNull() ?: return + val optionsClass = runCatching { + Class.forName(COMMAND_CLIENT_OPTIONS_CLASS) + }.getOrNull() ?: return + val clientClass = runCatching { + Class.forName(COMMAND_CLIENT_CLASS) + }.getOrNull() ?: return + + val handler = Proxy.newProxyInstance( + handlerInterface.classLoader, + arrayOf(handlerInterface), + commandClientHandler() + ) + val options = optionsClass.getConstructor().newInstance() + // Subscribe to the log stream (CommandLog == 0 in sing-box/libbox). + invoke(options, "addCommand", 0) + // Subscribe to connection events (CommandConnections == 4). Some + // transports, including WireGuard endpoint routing, don't emit useful + // per-connection lines through the regular log stream. + invoke(options, "addCommand", 4) + val client = clientClass + .getConstructor(handlerInterface, optionsClass) + .newInstance(handler, options) + commandClient = client + + // Connect dials the local command socket with retries; do it off the + // platform thread so we don't block start(). + val thread = Thread({ + runCatching { invoke(client, "connect") } + .onFailure { error -> + logSink?.invoke(3, "sing-box log stream connection failed: ${error.message}") + } + }, "singbox-log-client") + thread.isDaemon = true + thread.start() + logClientThread = thread + } + + private fun disconnectLogClient() { + val client = commandClient ?: return + commandClient = null + runCatching { invoke(client, "disconnect") } + logClientThread = null + } + + private fun commandClientHandler(): InvocationHandler { + return InvocationHandler { _, method, args -> + when (method.name) { + "writeLogs" -> { + val iterator = args?.firstOrNull() + if (iterator != null) { + forwardLogIterator(iterator) + } + null + } + "clearLogs", + "connected", + "disconnected", + "setDefaultLogLevel", + "writeStatus", + "writeGroups", + "writeOutbounds", + "initializeClashMode", + "updateClashMode" -> null + "writeConnectionEvents" -> { + val events = args?.firstOrNull() + if (events != null) { + forwardConnectionEvents(events) + } + null + } + else -> defaultValue(method.returnType, args) + } + } + } + + private fun forwardLogIterator(iterator: Any) { + val sink = logSink ?: return + runCatching { + while (invoke(iterator, "hasNext") as? Boolean == true) { + val entry = invoke(iterator, "next") ?: continue + val level = (runCatching { invoke(entry, "getLevel") }.getOrNull() as? Number) + ?.toInt() ?: 0 + val message = runCatching { invoke(entry, "getMessage") } + .getOrNull() as? String ?: continue + sink(level, message) + } + } + } + + private fun forwardConnectionEvents(events: Any) { + val sink = logSink ?: return + runCatching { + val iterator = invokeFirstAvailableResult(events, listOf("iterator", "Iterator")) ?: return + while (invoke(iterator, "hasNext") as? Boolean == true) { + val event = invoke(iterator, "next") ?: continue + val type = (invokeFirstAvailableResult(event, listOf("getType", "type")) as? Number) + ?.toInt() ?: continue + if (type != CONNECTION_EVENT_NEW && type != CONNECTION_EVENT_CLOSED) continue + + val connection = invokeFirstAvailableResult( + event, + listOf("getConnection", "connection"), + ) ?: continue + val eventLabel = if (type == CONNECTION_EVENT_CLOSED) "closed" else "opened" + sink(4, "connection $eventLabel ${describeConnection(connection)}") + } + } + } + + private fun describeConnection(connection: Any): String { + val network = stringValue(connection, "getNetwork", "network") + val source = stringValue(connection, "getSource", "source") + val destination = stringValue( + connection, + "displayDestination", + "DisplayDestination", + "getDestination", + "destination", + ) + val outbound = stringValue(connection, "getOutbound", "outbound") + val inbound = stringValue(connection, "getInbound", "inbound") + + return buildString { + if (network.isNotBlank()) append(network).append(' ') + if (source.isNotBlank()) append(source).append(" -> ") + append(destination.ifBlank { "unknown destination" }) + if (outbound.isNotBlank()) append(" via ").append(outbound) + if (inbound.isNotBlank()) append(" (").append(inbound).append(')') + } + } + + private fun stringValue(target: Any, vararg methodNames: String): String { + return invokeFirstAvailableResult(target, methodNames.toList()) as? String ?: "" + } + + private fun createLocalDnsTransport(): Any? { + val iface = runCatching { + Class.forName(LOCAL_DNS_TRANSPORT_CLASS) + }.getOrNull() ?: return null + + return Proxy.newProxyInstance( + iface.classLoader, + arrayOf(iface), + ) { _, method, args -> + when (method.name) { + "raw" -> true + "exchange" -> { + val ctx = args?.getOrNull(0) + val request = args?.getOrNull(1) as? ByteArray + if (ctx != null && request != null) { + runDohExchange(ctx, request) + } + null + } + // Lookup is only reachable when raw() returns false. We + // always return true above, so this path is dead. + "lookup" -> null + else -> defaultValue(method.returnType, args) + } + } + } + + private fun runDohExchange(ctx: Any, request: ByteArray) { + val url = bootstrapDohUrl + if (url == null) { + // No bootstrap URL configured — return SERVFAIL so sing-box gets a + // clean failure instead of hanging on a half-initialized bridge. + invokeIfAvailable(ctx, "errorCode", DNS_RCODE_SERVFAIL) + return + } + + try { + val response = dohResolver.exchange(url, request) + invokeIfAvailable(ctx, "rawSuccess", response) + } catch (error: Throwable) { + logSink?.invoke(3, "DoH bootstrap exchange failed: ${error.message}") + invokeIfAvailable(ctx, "errorCode", DNS_RCODE_SERVFAIL) + } + } + + private fun emptyStringIterator(): Any = emptyIterator(STRING_ITERATOR_CLASS) + + private fun emptyIterator(interfaceName: String): Any { + val iteratorInterface = Class.forName(interfaceName) + return Proxy.newProxyInstance( + iteratorInterface.classLoader, + arrayOf(iteratorInterface), + ) { _, method, _ -> + when (method.name) { + "hasNext" -> false + "len" -> 0 + "next" -> null + else -> defaultValue(method.returnType) + } + } + } + + private fun newInstance(className: String): Any { + return Class.forName(className).getConstructor().newInstance() + } + + private fun invoke(target: Any, methodName: String, vararg args: Any?): Any? { + val method = findMethod(target.javaClass, methodName, args.size) + return method.invoke(target, *args) + } + + private fun invokeIfAvailable(target: Any, methodName: String, vararg args: Any?) { + val method = target.javaClass.methods.firstOrNull { candidate -> + candidate.name == methodName && candidate.parameterTypes.size == args.size + } + method?.invoke(target, *args) + } + + private fun invokeFirstAvailable( + target: Any, + methodNames: List, + args: Array, + ) { + for (name in methodNames) { + val method = target.javaClass.methods.firstOrNull { candidate -> + candidate.name == name && candidate.parameterTypes.size == args.size + } + if (method != null) { + method.invoke(target, *args) + return + } + } + } + + private fun invokeFirstAvailableResult( + target: Any, + methodNames: List, + vararg args: Any?, + ): Any? { + for (name in methodNames) { + val method = target.javaClass.methods.firstOrNull { candidate -> + candidate.name == name && candidate.parameterTypes.size == args.size + } + if (method != null) { + return method.invoke(target, *args) + } + } + return null + } + + private fun findMethod(clazz: Class<*>, methodName: String, argCount: Int): Method { + return clazz.methods.firstOrNull { method -> + method.name == methodName && method.parameterTypes.size == argCount + } ?: throw NoSuchMethodError( + "${clazz.name}.$methodName($argCount args) is missing — linked libbox AAR " + + "may be incompatible. Available '${methodName}' overloads: " + + clazz.methods + .filter { it.name == methodName } + .joinToString { "${it.name}(${it.parameterTypes.joinToString { p -> p.simpleName }})" } + .ifEmpty { "" } + ) + } + + private fun defaultValue(returnType: Class<*>, args: Array? = null): Any? { + return when (returnType) { + java.lang.Boolean.TYPE -> false + java.lang.Integer.TYPE -> 0 + java.lang.Long.TYPE -> 0L + java.lang.Float.TYPE -> 0f + java.lang.Double.TYPE -> 0.0 + java.lang.Void.TYPE -> null + else -> args?.firstOrNull() + } + } + + private companion object { + const val LIBBOX_CLASS = "io.nekohasekai.libbox.Libbox" + const val SETUP_OPTIONS_CLASS = "io.nekohasekai.libbox.SetupOptions" + const val COMMAND_SERVER_CLASS = "io.nekohasekai.libbox.CommandServer" + const val COMMAND_SERVER_HANDLER_CLASS = "io.nekohasekai.libbox.CommandServerHandler" + const val PLATFORM_INTERFACE_CLASS = "io.nekohasekai.libbox.PlatformInterface" + const val LOCAL_DNS_TRANSPORT_CLASS = "io.nekohasekai.libbox.LocalDNSTransport" + const val DNS_RCODE_SERVFAIL = 2 + const val OVERRIDE_OPTIONS_CLASS = "io.nekohasekai.libbox.OverrideOptions" + const val STRING_ITERATOR_CLASS = "io.nekohasekai.libbox.StringIterator" + const val NETWORK_INTERFACE_ITERATOR_CLASS = "io.nekohasekai.libbox.NetworkInterfaceIterator" + const val CONNECTION_OWNER_CLASS = "io.nekohasekai.libbox.ConnectionOwner" + const val SYSTEM_PROXY_STATUS_CLASS = "io.nekohasekai.libbox.SystemProxyStatus" + const val WIFI_STATE_CLASS = "io.nekohasekai.libbox.WIFIState" + const val COMMAND_CLIENT_CLASS = "io.nekohasekai.libbox.CommandClient" + const val COMMAND_CLIENT_HANDLER_CLASS = "io.nekohasekai.libbox.CommandClientHandler" + const val COMMAND_CLIENT_OPTIONS_CLASS = "io.nekohasekai.libbox.CommandClientOptions" + const val CONNECTION_EVENT_NEW = 0 + const val CONNECTION_EVENT_CLOSED = 2 + } +} diff --git a/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/PlatformDohResolver.kt b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/PlatformDohResolver.kt new file mode 100644 index 00000000..ca6987a1 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/PlatformDohResolver.kt @@ -0,0 +1,75 @@ +package eu.weblibre.flutter_singbox_proxy + +import java.io.IOException +import java.util.concurrent.TimeUnit +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * RFC 8484 DoH client used as the bootstrap resolver for sing-box's + * `type: "local"` DNS transport. + * + * Sing-box calls the platform LocalDNSTransport whenever a DNS server's own + * hostname (or any other hostname referenced by `domain_resolver` / + * `default_domain_resolver`) needs resolving. We bounce the wire-format query + * straight to a configured DoH endpoint so no query ever hits Android's + * system resolver. + * + * The DoH endpoint's own hostname is resolved exactly once by the JVM HTTP + * stack. Configure the URL with an IP literal (e.g. `https://1.1.1.1/dns-query`) + * if even that one lookup must not leak. + */ +class PlatformDohResolver( + private val connectTimeoutMillis: Int = 5_000, + private val readTimeoutMillis: Int = 5_000, +) { + private val client: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(connectTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) + .readTimeout(readTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) + .build() + + /** Send [request] as a DoH POST and return the raw DNS wire-format reply. */ + @Throws(IOException::class) + fun exchange(url: String, request: ByteArray): ByteArray { + val httpRequest = Request.Builder() + .url(url) + .header("Accept", DNS_MESSAGE_MIME) + .post(request.toRequestBody(DNS_MESSAGE_MEDIA_TYPE)) + .build() + + client.newCall(httpRequest).execute().use { response -> + if (!response.isSuccessful) { + throw IOException( + "DoH endpoint returned HTTP ${response.code} via ${response.protocol}" + ) + } + val contentType = response.header("Content-Type").orEmpty() + if (!contentType.startsWith(DNS_MESSAGE_MIME, ignoreCase = true)) { + throw IOException( + "DoH endpoint returned unexpected Content-Type: $contentType" + ) + } + + val body = response.body ?: throw IOException("DoH endpoint returned no body") + val contentLength = body.contentLength() + if (contentLength > MAX_DNS_RESPONSE) { + throw IOException("DoH response exceeds maximum DNS message size") + } + + return body.bytes().also { bytes -> + if (bytes.size > MAX_DNS_RESPONSE) { + throw IOException("DoH response exceeds maximum DNS message size") + } + } + } + } + + private companion object { + const val DNS_MESSAGE_MIME = "application/dns-message" + val DNS_MESSAGE_MEDIA_TYPE = DNS_MESSAGE_MIME.toMediaType() + // EDNS0 typically caps responses at 4 KiB; we allow a little slack. + const val MAX_DNS_RESPONSE = 8 * 1024 + } +} diff --git a/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxConfigBuilder.kt b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxConfigBuilder.kt new file mode 100644 index 00000000..d3d1253e --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxConfigBuilder.kt @@ -0,0 +1,398 @@ +package eu.weblibre.flutter_singbox_proxy + +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyConfigResult +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyDnsConfig +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyDnsServerConfig +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyProfile +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyProfileType +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeEndpoint +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeOptions +import java.net.URI +import java.security.SecureRandom +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +private const val LOCALHOST = "127.0.0.1" +private const val DEFAULT_BASE_PORT = 12000L +private const val BASE64_URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + +/** + * Tag of the always-emitted `type: "local"` DNS server. Hooked at runtime by + * our LocalDNSTransport bridge (PlatformDohResolver), so every hostname in + * the config — including DoH endpoint hostnames and WireGuard peer + * hostnames — resolves through DoH instead of `/etc/resolv.conf`. + */ +private const val DNS_BOOTSTRAP_TAG = "local" + +class SingboxConfigBuilder( + private val random: SecureRandom = SecureRandom() +) { + fun validateProfile(profile: SingboxProxyProfile): String? { + if (profile.id.isBlank()) return "Profile id is required." + if (profile.name.isBlank()) return "Profile name is required." + + val outbound = try { + buildOutbound(profile) + } catch (error: JSONException) { + return "Invalid outbound JSON: ${error.message}" + } catch (error: IllegalArgumentException) { + return error.message + } + + val expectedType = expectedOutboundType(profile.type) + val actualType = outbound.optString("type") + if (expectedType != null && actualType != expectedType) { + return "Profile type ${profile.type.name} requires sing-box outbound type '$expectedType'." + } + + return null + } + + fun build( + profiles: List, + options: SingboxProxyRuntimeOptions + ): SingboxProxyConfigResult { + profiles.forEach { profile -> + validateProfile(profile)?.let { throw IllegalArgumentException(it) } + } + + val inbounds = JSONArray() + val endpointsJson = JSONArray() + val outbounds = JSONArray() + val rules = JSONArray() + val endpoints = mutableListOf() + val basePort = options.preferredBasePort ?: DEFAULT_BASE_PORT + + profiles.forEachIndexed { index, profile -> + val inboundTag = inboundTag(profile.id) + val outboundTag = outboundTag(profile.id) + val port = basePort + index + val username = generateToken("u") + val password = generateToken("p") + + inbounds.put(JSONObject().apply { + put("type", "socks") + put("tag", inboundTag) + put("listen", LOCALHOST) + put("listen_port", port) + put("users", JSONArray().put(JSONObject().apply { + put("username", username) + put("password", password) + })) + }) + + if (profile.type == SingboxProxyProfileType.WIREGUARD) { + endpointsJson.put(buildWireGuardEndpoint(profile, outboundTag)) + } else { + outbounds.put(buildOutbound(profile).apply { + put("tag", outboundTag) + }) + } + + rules.put(JSONObject().apply { + put("inbound", JSONArray().put(inboundTag)) + put("action", "route") + put("outbound", outboundTag) + }) + + endpoints += SingboxProxyRuntimeEndpoint( + profileId = profile.id, + host = LOCALHOST, + port = port, + username = username, + password = password + ) + } + + val finalOutbound = if (options.blockUnmatchedTraffic) "block" else "direct" + outbounds.put(JSONObject().apply { + put("type", finalOutbound) + put("tag", finalOutbound) + }) + // Always expose a `direct` outbound even when blockUnmatchedTraffic = + // true so internal bootstrap/fallback paths still have a direct route. + if (finalOutbound != "direct") { + outbounds.put(JSONObject().apply { + put("type", "direct") + put("tag", "direct") + }) + } + + val dnsBlock = options.dnsConfig?.let(::buildDnsBlock) + if (dnsBlock != null && options.bootstrapDohUrl.isNullOrBlank()) { + throw IllegalArgumentException( + "bootstrapDohUrl is required when dnsConfig is provided." + ) + } + + val config = JSONObject().apply { + put("log", JSONObject().apply { put("level", "info") }) + put("inbounds", inbounds) + if (endpointsJson.length() > 0) { + put("endpoints", endpointsJson) + } + put("outbounds", outbounds) + put("route", JSONObject().apply { + put("rules", rules) + put("final", finalOutbound) + // When DNS is configured, route everything's hostname + // resolution through the platform LocalDNSTransport bridge so + // WireGuard peer hostnames and any other outbound-dialer + // hostname go through DoH, not /etc/resolv.conf. + if (dnsBlock != null) { + put("default_domain_resolver", DNS_BOOTSTRAP_TAG) + } + }) + dnsBlock?.let { put("dns", it) } + } + + return SingboxProxyConfigResult( + configJson = config.toString(2), + endpoints = endpoints + ) + } + + private fun buildDnsBlock(dns: SingboxProxyDnsConfig): JSONObject? { + if (dns.servers.isEmpty()) return null + + val serversJson = JSONArray() + val rulesJson = JSONArray() + + // Always emit the platform `local` server first. It is the foundation + // every other server's `domain_resolver` (and the route's + // default_domain_resolver) points at, and the LocalDNSTransport + // bridge backs it with DoH at runtime. + serversJson.put(JSONObject().apply { + put("type", "local") + put("tag", DNS_BOOTSTRAP_TAG) + }) + + for (server in dns.servers) { + serversJson.put(buildDnsServer(server)) + + if (server.matchDomainSuffixes.isNotEmpty() || + server.matchInbounds.isNotEmpty() + ) { + rulesJson.put(JSONObject().apply { + if (server.matchDomainSuffixes.isNotEmpty()) { + put( + "domain_suffix", + JSONArray(server.matchDomainSuffixes) + ) + } + if (server.matchInbounds.isNotEmpty()) { + put("inbound", JSONArray(server.matchInbounds)) + } + put("action", "route") + put("server", server.tag) + }) + } + } + + return JSONObject().apply { + put("servers", serversJson) + if (rulesJson.length() > 0) { + put("rules", rulesJson) + } + if (dns.domainStrategy.isNotBlank()) { + put("strategy", dns.domainStrategy) + } + val finalTag = dns.finalServerTag ?: DNS_BOOTSTRAP_TAG + put("final", finalTag) + } + } + + private fun buildDnsServer(server: SingboxProxyDnsServerConfig): JSONObject { + val parsed = parseDnsAddress(server.address) + return JSONObject().apply { + put("type", parsed.type) + put("tag", server.tag) + parsed.server?.let { put("server", it) } + parsed.serverPort?.let { put("server_port", it) } + parsed.path?.let { put("path", it) } + server.detourTag?.takeUnless { it == "direct" }?.let { put("detour", it) } + // Hostname targets always bootstrap through `local`; sing-box + // ignores `domain_resolver` for IP-literal servers, so emitting + // it unconditionally is fine and keeps the JSON uniform. + if (parsed.server != null && !parsed.serverIsIpLiteral) { + put("domain_resolver", DNS_BOOTSTRAP_TAG) + } + } + } + + private fun parseDnsAddress(address: String): ParsedDnsAddress { + val trimmed = address.trim() + if (trimmed == "local") { + return ParsedDnsAddress(type = "local") + } + + val uri = if (trimmed.contains("://")) URI(trimmed) else null + val scheme = uri?.scheme?.lowercase() + return when (scheme) { + null -> parseHostPort(trimmed, "udp", 53) + "udp" -> parseUriHostPort(uri!!, "udp", 53) + "tcp" -> parseUriHostPort(uri!!, "tcp", 53) + "tls" -> parseUriHostPort(uri!!, "tls", 853) + "quic" -> parseUriHostPort(uri!!, "quic", 853) + "https", "h3" -> parseUriHostPort(uri, scheme, 443).copy( + path = uri.path.takeUnless { it.isNullOrBlank() || it == "/dns-query" } + ) + else -> throw IllegalArgumentException("Unsupported DNS server scheme: $scheme") + } + } + + private fun parseUriHostPort(uri: URI, type: String, defaultPort: Int): ParsedDnsAddress { + val host = uri.host ?: throw IllegalArgumentException("Invalid DNS server address") + val bare = host.removePrefix("[").removeSuffix("]") + return ParsedDnsAddress( + type = type, + server = bare, + serverPort = uri.port.takeIf { it >= 0 && it != defaultPort }, + serverIsIpLiteral = isIpLiteral(bare), + ) + } + + private fun parseHostPort(value: String, type: String, defaultPort: Int): ParsedDnsAddress { + val trimmed = value.trim() + if (trimmed.isBlank()) throw IllegalArgumentException("DNS server address is required") + + val splitPort = trimmed.lastIndexOf(':') + val hasSingleColon = splitPort > 0 && trimmed.indexOf(':') == splitPort + val host = if (hasSingleColon) trimmed.substring(0, splitPort) else trimmed + val port = if (hasSingleColon) trimmed.substring(splitPort + 1).toIntOrNull() else null + val bare = host.removePrefix("[").removeSuffix("]") + + return ParsedDnsAddress( + type = type, + server = bare, + serverPort = port?.takeUnless { it == defaultPort }, + serverIsIpLiteral = isIpLiteral(bare), + ) + } + + private fun isIpLiteral(host: String): Boolean { + if (host.matches(Regex("^\\d{1,3}(\\.\\d{1,3}){3}$"))) return true + if (host.contains(":") && host.matches(Regex("^[0-9A-Fa-f:.]+$"))) return true + return false + } + + private data class ParsedDnsAddress( + val type: String, + val server: String? = null, + val serverPort: Int? = null, + val path: String? = null, + val serverIsIpLiteral: Boolean = false, + ) + + private fun buildOutbound(profile: SingboxProxyProfile): JSONObject { + val outbound = JSONObject(profile.configJson) + profile.secretJson?.takeIf { it.isNotBlank() }?.let { secretJson -> + deepMerge(outbound, JSONObject(secretJson)) + } + + expectedOutboundType(profile.type)?.let { expectedType -> + val actualType = outbound.optString("type") + if (actualType.isBlank()) { + outbound.put("type", expectedType) + } + } + + return outbound + } + + private fun buildWireGuardEndpoint(profile: SingboxProxyProfile, tag: String): JSONObject { + val endpoint = buildOutbound(profile) + endpoint.put("tag", tag) + + endpoint.remove("server")?.let { server -> + val peer = JSONObject().apply { + put("address", server) + endpoint.remove("server_port")?.let { put("port", it) } + endpoint.remove("peer_public_key")?.let { put("public_key", it) } + endpoint.remove("pre_shared_key")?.let { put("pre_shared_key", it) } + endpoint.remove("reserved")?.let { put("reserved", it) } + endpoint.remove("persistent_keepalive_interval")?.let { + put("persistent_keepalive_interval", it) + } + put("allowed_ips", endpoint.remove("allowed_ips") ?: JSONArray().apply { + put("0.0.0.0/0") + put("::/0") + }) + } + endpoint.put("peers", JSONArray().put(peer)) + } + + endpoint.remove("local_address")?.let { endpoint.put("address", it) } + endpoint.remove("system_interface")?.let { endpoint.put("system", it) } + endpoint.remove("interface_name")?.let { endpoint.put("name", it) } + endpoint.remove("gso") + + return endpoint + } + + private fun deepMerge(target: JSONObject, source: JSONObject) { + val keys = source.keys() + while (keys.hasNext()) { + val key = keys.next() + val value = source.get(key) + if (value is JSONObject && target.opt(key) is JSONObject) { + deepMerge(target.getJSONObject(key), value) + } else { + target.put(key, value) + } + } + } + + private fun generateToken(prefix: String): String { + val bytes = ByteArray(18) + random.nextBytes(bytes) + return prefix + base64UrlNoPadding(bytes) + } + + private fun base64UrlNoPadding(bytes: ByteArray): String { + val output = StringBuilder((bytes.size * 4 + 2) / 3) + var index = 0 + while (index < bytes.size) { + val b0 = bytes[index++].toInt() and 0xff + val b1 = if (index < bytes.size) bytes[index++].toInt() and 0xff else -1 + val b2 = if (index < bytes.size) bytes[index++].toInt() and 0xff else -1 + + output.append(BASE64_URL_ALPHABET[b0 ushr 2]) + if (b1 < 0) { + output.append(BASE64_URL_ALPHABET[(b0 and 0x03) shl 4]) + } else { + output.append(BASE64_URL_ALPHABET[((b0 and 0x03) shl 4) or (b1 ushr 4)]) + if (b2 < 0) { + output.append(BASE64_URL_ALPHABET[(b1 and 0x0f) shl 2]) + } else { + output.append(BASE64_URL_ALPHABET[((b1 and 0x0f) shl 2) or (b2 ushr 6)]) + output.append(BASE64_URL_ALPHABET[b2 and 0x3f]) + } + } + } + return output.toString() + } + + private fun inboundTag(profileId: String) = SingboxTagFormat.inboundTag(profileId) + + private fun outboundTag(profileId: String) = SingboxTagFormat.outboundTag(profileId) + + private fun expectedOutboundType(type: SingboxProxyProfileType): String? = when (type) { + SingboxProxyProfileType.SOCKS -> "socks" + SingboxProxyProfileType.HTTP -> "http" + SingboxProxyProfileType.SHADOWSOCKS -> "shadowsocks" + SingboxProxyProfileType.VMESS -> "vmess" + SingboxProxyProfileType.VLESS -> "vless" + SingboxProxyProfileType.TROJAN -> "trojan" + SingboxProxyProfileType.NAIVE -> "naive" + SingboxProxyProfileType.HYSTERIA -> "hysteria" + SingboxProxyProfileType.HYSTERIA2 -> "hysteria2" + SingboxProxyProfileType.TUIC -> "tuic" + SingboxProxyProfileType.SSH -> "ssh" + SingboxProxyProfileType.WIREGUARD -> "wireguard" + SingboxProxyProfileType.SHADOW_TLS -> "shadowtls" + SingboxProxyProfileType.ANY_TLS -> "anytls" + SingboxProxyProfileType.CUSTOM_OUTBOUND -> null + } +} diff --git a/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxRuntimeManager.kt b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxRuntimeManager.kt new file mode 100644 index 00000000..d1b8bb40 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxRuntimeManager.kt @@ -0,0 +1,286 @@ +package eu.weblibre.flutter_singbox_proxy + +import android.content.Context +import android.os.Handler +import android.os.Looper +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyApi +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyConfigResult +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyLogMessage +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyProfile +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeOptions +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeEndpoint +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeState +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeStatus +import java.util.concurrent.Executors + +class SingboxRuntimeManager( + context: Context, + private val configBuilder: SingboxConfigBuilder = SingboxConfigBuilder(), + private val libboxRuntime: LibboxRuntime = LibboxRuntime(context), + private val onStateChanged: (SingboxProxyRuntimeState) -> Unit = {}, + private val onLogMessage: (SingboxProxyLogMessage) -> Unit = {}, + private val dispatchToMain: ((() -> Unit) -> Unit) = { action -> + if (Looper.myLooper() == Looper.getMainLooper()) { + action() + } else { + Handler(Looper.getMainLooper()).post(action) + } + } +) : SingboxProxyApi { + // Pigeon dispatches Dart-side calls on the platform thread, but libbox + // callbacks fire from native threads. Guard all state transitions with + // a single lock so concurrent stop / start / event paths cannot tear + // activeProfiles, activeOptions, or `state` against each other. + private val stateLock = Any() + private val runtimeExecutor = Executors.newSingleThreadExecutor { runnable -> + Thread(runnable, "singbox-runtime").apply { isDaemon = true } + } + + private var state = SingboxProxyRuntimeState( + status = SingboxProxyRuntimeStatus.STOPPED, + endpoints = emptyList(), + message = null + ) + private var activeProfiles = emptyList() + private var activeOptions = SingboxProxyRuntimeOptions( + preferredBasePort = null, + blockUnmatchedTraffic = true + ) + + init { + // Forward every sing-box log entry to Dart. libbox levels follow + // sing/common/logger: 0 = panic, 1 = fatal, 2 = error, 3 = warn, + // 4 = info, 5 = debug, 6 = trace. + libboxRuntime.setLogSink { level, message -> + emitLogMessage(level, message) + } + } + + private fun libboxLogLevelName(level: Int): String = when (level) { + 0 -> "panic" + 1 -> "fatal" + 2 -> "error" + 3 -> "warn" + 4 -> "info" + 5 -> "debug" + 6 -> "trace" + else -> "info" + } + + private fun emitLogMessage(level: Int, message: String) { + emitLogMessage( + SingboxProxyLogMessage( + level = libboxLogLevelName(level), + message = message, + timestamp = System.currentTimeMillis(), + profileId = null + ) + ) + } + + private fun emitLogMessage(logMessage: SingboxProxyLogMessage) { + dispatchToMain { + onLogMessage(logMessage) + } + } + + private fun emitStateChanged(nextState: SingboxProxyRuntimeState) { + dispatchToMain { + onStateChanged(nextState) + } + } + + private fun statusLogMessage( + status: SingboxProxyRuntimeStatus, + message: String + ) = SingboxProxyLogMessage( + level = if (status == SingboxProxyRuntimeStatus.ERROR) "warn" else "info", + message = message, + timestamp = System.currentTimeMillis(), + profileId = null + ) + + override fun validateProfile( + profile: SingboxProxyProfile, + callback: (Result) -> Unit + ) { + callback(Result.success(configBuilder.validateProfile(profile))) + } + + override fun buildConfig( + profiles: List, + options: SingboxProxyRuntimeOptions, + callback: (Result) -> Unit + ) { + runCatching { configBuilder.build(profiles, options) } + .onSuccess { callback(Result.success(it)) } + .onFailure { callback(Result.failure(it)) } + } + + override fun start( + profiles: List, + options: SingboxProxyRuntimeOptions, + callback: (Result) -> Unit + ) { + runtimeExecutor.execute { + val result = synchronized(stateLock) { + val previousState = state + runCatching { + updateStateLocked( + SingboxProxyRuntimeStatus.STARTING, + emptyList(), + "Building sing-box config" + ) + val config = configBuilder.build(profiles, options) + + if (!libboxRuntime.isAvailable()) { + val message = "sing-box libbox runtime is not linked" + updateStateLocked( + SingboxProxyRuntimeStatus.ERROR, + emptyList(), + message + ) + throw IllegalStateException(message) + } + + val previousBootstrapDohUrl = activeOptions.bootstrapDohUrl + libboxRuntime.setBootstrapDohUrl(options.bootstrapDohUrl) + try { + libboxRuntime.start(config.configJson) + } catch (error: Throwable) { + libboxRuntime.setBootstrapDohUrl(previousBootstrapDohUrl) + throw error + } + // Only commit profiles/options after start() returns without + // throwing, so a failed start leaves the previous active set + // intact rather than half-replaced. + activeProfiles = profiles + activeOptions = options + updateStateLocked( + SingboxProxyRuntimeStatus.RUNNING, + config.endpoints, + null + ) + state + }.onFailure { error -> + updateStateLocked( + SingboxProxyRuntimeStatus.ERROR, + previousState.endpoints, + error.message ?: error::class.java.simpleName + ) + } + } + dispatchToMain { callback(result) } + } + } + + override fun stop(profileIds: List, callback: (Result) -> Unit) { + runtimeExecutor.execute { + val result = synchronized(stateLock) { + runCatching { + val remaining = activeProfiles.filterNot { profile -> + profile.id in profileIds + } + if (remaining.isEmpty()) { + libboxRuntime.stopService() + activeProfiles = emptyList() + activeOptions = defaultRuntimeOptions() + libboxRuntime.setBootstrapDohUrl(null) + updateStateLocked( + SingboxProxyRuntimeStatus.STOPPED, + emptyList(), + null + ) + } else { + val config = configBuilder.build(remaining, activeOptions) + // Partial stop keeps activeOptions.bootstrapDohUrl, so + // no setBootstrapDohUrl call is needed here — the + // libbox bridge already holds the right URL from the + // most recent start(). + libboxRuntime.start(config.configJson) + activeProfiles = remaining + updateStateLocked( + SingboxProxyRuntimeStatus.RUNNING, + config.endpoints, + null + ) + } + }.onFailure { error -> + updateStateLocked( + SingboxProxyRuntimeStatus.ERROR, + state.endpoints, + error.message ?: error::class.java.simpleName + ) + } + } + dispatchToMain { callback(result) } + } + } + + override fun stopAll(callback: (Result) -> Unit) { + runtimeExecutor.execute { + val result = synchronized(stateLock) { + runCatching { + libboxRuntime.stopService() + activeProfiles = emptyList() + activeOptions = defaultRuntimeOptions() + libboxRuntime.setBootstrapDohUrl(null) + updateStateLocked( + SingboxProxyRuntimeStatus.STOPPED, + emptyList(), + null + ) + }.onFailure { error -> + updateStateLocked( + SingboxProxyRuntimeStatus.ERROR, + state.endpoints, + error.message ?: error::class.java.simpleName + ) + } + } + dispatchToMain { callback(result) } + } + } + + override fun getState(): SingboxProxyRuntimeState = synchronized(stateLock) { state } + + fun close() { + runtimeExecutor.execute { + synchronized(stateLock) { + runCatching { libboxRuntime.stopService() } + activeProfiles = emptyList() + activeOptions = defaultRuntimeOptions() + libboxRuntime.setBootstrapDohUrl(null) + libboxRuntime.close() + state = SingboxProxyRuntimeState( + status = SingboxProxyRuntimeStatus.STOPPED, + endpoints = emptyList(), + message = null + ) + } + } + runtimeExecutor.shutdown() + } + + private fun updateStateLocked( + status: SingboxProxyRuntimeStatus, + endpoints: List, + message: String? + ) { + state = SingboxProxyRuntimeState( + status = status, + endpoints = endpoints, + message = message + ) + val snapshot = state + emitStateChanged(snapshot) + message?.let { emitLogMessage(statusLogMessage(status, it)) } + } + + private fun defaultRuntimeOptions() = SingboxProxyRuntimeOptions( + preferredBasePort = null, + blockUnmatchedTraffic = true, + dnsConfig = null, + bootstrapDohUrl = null + ) +} diff --git a/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxTagFormat.kt b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxTagFormat.kt new file mode 100644 index 00000000..bd241173 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxTagFormat.kt @@ -0,0 +1,17 @@ +package eu.weblibre.flutter_singbox_proxy + +/** + * Inbound/outbound tag format shared between the Kotlin config builder and + * the Dart DNS resolver. Changing this contract requires updating + * `dns_config_resolver.dart` in lockstep — the Dart side emits matching tags + * so DNS detours bind to the outbound this builder actually creates. + */ +internal object SingboxTagFormat { + fun inboundTag(profileId: String): String = "in-${sanitizeTag(profileId)}" + + fun outboundTag(profileId: String): String = "out-${sanitizeTag(profileId)}" + + fun sanitizeTag(value: String): String { + return value.replace(Regex("[^A-Za-z0-9_.-]"), "_") + } +} diff --git a/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/generated/SingboxProxyApi.g.kt b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/generated/SingboxProxyApi.g.kt new file mode 100644 index 00000000..d4ab70c5 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/generated/SingboxProxyApi.g.kt @@ -0,0 +1,947 @@ +// Autogenerated from Pigeon (v26.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package eu.weblibre.flutter_singbox_proxy.generated + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object SingboxProxyApiPigeonUtils { + + fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") } + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true + } + if (a is Array<*> && b is Array<*>) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true + } + if (a is List<*> && b is List<*>) { + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true + } + if (a is Map<*, *> && b is Map<*, *>) { + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false + } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) + } + return a == b + } + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +enum class SingboxProxyProfileType(val raw: Int) { + SOCKS(0), + HTTP(1), + SHADOWSOCKS(2), + VMESS(3), + VLESS(4), + TROJAN(5), + NAIVE(6), + HYSTERIA(7), + HYSTERIA2(8), + TUIC(9), + SSH(10), + WIREGUARD(11), + SHADOW_TLS(12), + ANY_TLS(13), + CUSTOM_OUTBOUND(14); + + companion object { + fun ofRaw(raw: Int): SingboxProxyProfileType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class SingboxProxyRuntimeStatus(val raw: Int) { + STOPPED(0), + STARTING(1), + RUNNING(2), + STOPPING(3), + ERROR(4); + + companion object { + fun ofRaw(raw: Int): SingboxProxyRuntimeStatus? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SingboxProxyProfile ( + val id: String, + val name: String, + val type: SingboxProxyProfileType, + /** + * Public profile configuration as JSON. The schema is intentionally owned by + * the profile type so the Pigeon API stays stable while sing-box evolves. + */ + val configJson: String, + /** + * Resolved secret values as JSON. Flutter stores secrets independently and + * only passes them to native code when building or starting a runtime config. + */ + val secretJson: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): SingboxProxyProfile { + val id = pigeonVar_list[0] as String + val name = pigeonVar_list[1] as String + val type = pigeonVar_list[2] as SingboxProxyProfileType + val configJson = pigeonVar_list[3] as String + val secretJson = pigeonVar_list[4] as String? + return SingboxProxyProfile(id, name, type, configJson, secretJson) + } + } + fun toList(): List { + return listOf( + id, + name, + type, + configJson, + secretJson, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as SingboxProxyProfile + return SingboxProxyApiPigeonUtils.deepEquals(this.id, other.id) && SingboxProxyApiPigeonUtils.deepEquals(this.name, other.name) && SingboxProxyApiPigeonUtils.deepEquals(this.type, other.type) && SingboxProxyApiPigeonUtils.deepEquals(this.configJson, other.configJson) && SingboxProxyApiPigeonUtils.deepEquals(this.secretJson, other.secretJson) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.id) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.name) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.type) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.configJson) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.secretJson) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SingboxProxyRuntimeOptions ( + /** Optional preferred start port for generated local SOCKS inbounds. */ + val preferredBasePort: Long? = null, + /** + * If true, traffic entering sing-box without a matching inbound rule is + * rejected instead of falling through to direct. + */ + val blockUnmatchedTraffic: Boolean, + /** + * Optional DNS block emitted into sing-box config. When null, sing-box + * uses its built-in default (system resolver), which can leak DNS outside + * the proxy — callers should always provide an explicit configuration. + */ + val dnsConfig: SingboxProxyDnsConfig? = null, + /** + * DoH endpoint used by the native LocalDNSTransport bridge to bootstrap + * hostname-only DNS server addresses (and any other hostname appearing in + * the sing-box config). When null, the bridge refuses to resolve and + * sing-box's stock `/etc/resolv.conf`/127.0.0.1:53 path runs — which is + * broken on Android. Callers should always pass the browser DoH URL. + */ + val bootstrapDohUrl: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): SingboxProxyRuntimeOptions { + val preferredBasePort = pigeonVar_list[0] as Long? + val blockUnmatchedTraffic = pigeonVar_list[1] as Boolean + val dnsConfig = pigeonVar_list[2] as SingboxProxyDnsConfig? + val bootstrapDohUrl = pigeonVar_list[3] as String? + return SingboxProxyRuntimeOptions(preferredBasePort, blockUnmatchedTraffic, dnsConfig, bootstrapDohUrl) + } + } + fun toList(): List { + return listOf( + preferredBasePort, + blockUnmatchedTraffic, + dnsConfig, + bootstrapDohUrl, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as SingboxProxyRuntimeOptions + return SingboxProxyApiPigeonUtils.deepEquals(this.preferredBasePort, other.preferredBasePort) && SingboxProxyApiPigeonUtils.deepEquals(this.blockUnmatchedTraffic, other.blockUnmatchedTraffic) && SingboxProxyApiPigeonUtils.deepEquals(this.dnsConfig, other.dnsConfig) && SingboxProxyApiPigeonUtils.deepEquals(this.bootstrapDohUrl, other.bootstrapDohUrl) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.preferredBasePort) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.blockUnmatchedTraffic) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.dnsConfig) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.bootstrapDohUrl) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SingboxProxyDnsServerConfig ( + /** sing-box server tag, used to reference the server from `dns.rules`. */ + val tag: String, + /** + * Server address. `https://...`, `tls://...`, `quic://...`, or plain IP. + * Hostnames are resolved on demand via the platform LocalDNSTransport + * bridge (sing-box `type: "local"` with our DoH-backed implementation). + */ + val address: String, + /** + * Outbound tag to dial the resolver through, or `direct` for direct, or + * null when sing-box should pick automatically. + */ + val detourTag: String? = null, + /** + * If non-empty, attaches a `dns.rules` entry routing matching domains to + * this server. + */ + val matchDomainSuffixes: List, + /** Advanced sing-box geosite selectors (e.g. `geosite:cn`). */ + val matchGeosites: List, + /** + * If non-empty, attaches a `dns.rules` entry routing queries that *the + * listed outbounds* originate to this server. Note: sing-box treats an + * outbound's own bootstrap lookups (e.g. WireGuard peer hostname + * resolution) as queries from that outbound, so using this for per-profile + * scoping creates a chicken-and-egg loop at startup. Prefer + * [matchInbounds] for "queries from tabs routed through this profile". + */ + val matchOutbounds: List, + /** + * If non-empty, attaches a `dns.rules` entry matching the listed inbound + * tags. Queries entering via that inbound (e.g. a tab whose container is + * bound to this profile's local SOCKS inbound) resolve through this + * server. Endpoint-bootstrap lookups don't come from any inbound, so this + * scope safely excludes them. + */ + val matchInbounds: List +) + { + companion object { + fun fromList(pigeonVar_list: List): SingboxProxyDnsServerConfig { + val tag = pigeonVar_list[0] as String + val address = pigeonVar_list[1] as String + val detourTag = pigeonVar_list[2] as String? + val matchDomainSuffixes = pigeonVar_list[3] as List + val matchGeosites = pigeonVar_list[4] as List + val matchOutbounds = pigeonVar_list[5] as List + val matchInbounds = pigeonVar_list[6] as List + return SingboxProxyDnsServerConfig(tag, address, detourTag, matchDomainSuffixes, matchGeosites, matchOutbounds, matchInbounds) + } + } + fun toList(): List { + return listOf( + tag, + address, + detourTag, + matchDomainSuffixes, + matchGeosites, + matchOutbounds, + matchInbounds, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as SingboxProxyDnsServerConfig + return SingboxProxyApiPigeonUtils.deepEquals(this.tag, other.tag) && SingboxProxyApiPigeonUtils.deepEquals(this.address, other.address) && SingboxProxyApiPigeonUtils.deepEquals(this.detourTag, other.detourTag) && SingboxProxyApiPigeonUtils.deepEquals(this.matchDomainSuffixes, other.matchDomainSuffixes) && SingboxProxyApiPigeonUtils.deepEquals(this.matchGeosites, other.matchGeosites) && SingboxProxyApiPigeonUtils.deepEquals(this.matchOutbounds, other.matchOutbounds) && SingboxProxyApiPigeonUtils.deepEquals(this.matchInbounds, other.matchInbounds) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.tag) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.address) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.detourTag) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.matchDomainSuffixes) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.matchGeosites) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.matchOutbounds) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.matchInbounds) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SingboxProxyDnsConfig ( + val servers: List, + /** + * Server tag used as `dns.final`. When null, sing-box uses the first + * server in the list as the fallback. + */ + val finalServerTag: String? = null, + /** sing-box `dns.strategy` string. e.g. `prefer_ipv4`, `ipv4_only`. */ + val domainStrategy: String +) + { + companion object { + fun fromList(pigeonVar_list: List): SingboxProxyDnsConfig { + val servers = pigeonVar_list[0] as List + val finalServerTag = pigeonVar_list[1] as String? + val domainStrategy = pigeonVar_list[2] as String + return SingboxProxyDnsConfig(servers, finalServerTag, domainStrategy) + } + } + fun toList(): List { + return listOf( + servers, + finalServerTag, + domainStrategy, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as SingboxProxyDnsConfig + return SingboxProxyApiPigeonUtils.deepEquals(this.servers, other.servers) && SingboxProxyApiPigeonUtils.deepEquals(this.finalServerTag, other.finalServerTag) && SingboxProxyApiPigeonUtils.deepEquals(this.domainStrategy, other.domainStrategy) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.servers) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.finalServerTag) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.domainStrategy) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SingboxProxyRuntimeEndpoint ( + val profileId: String, + val host: String, + val port: Long, + val username: String, + val password: String +) + { + companion object { + fun fromList(pigeonVar_list: List): SingboxProxyRuntimeEndpoint { + val profileId = pigeonVar_list[0] as String + val host = pigeonVar_list[1] as String + val port = pigeonVar_list[2] as Long + val username = pigeonVar_list[3] as String + val password = pigeonVar_list[4] as String + return SingboxProxyRuntimeEndpoint(profileId, host, port, username, password) + } + } + fun toList(): List { + return listOf( + profileId, + host, + port, + username, + password, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as SingboxProxyRuntimeEndpoint + return SingboxProxyApiPigeonUtils.deepEquals(this.profileId, other.profileId) && SingboxProxyApiPigeonUtils.deepEquals(this.host, other.host) && SingboxProxyApiPigeonUtils.deepEquals(this.port, other.port) && SingboxProxyApiPigeonUtils.deepEquals(this.username, other.username) && SingboxProxyApiPigeonUtils.deepEquals(this.password, other.password) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.profileId) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.host) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.port) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.username) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.password) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SingboxProxyRuntimeState ( + val status: SingboxProxyRuntimeStatus, + val endpoints: List, + val message: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): SingboxProxyRuntimeState { + val status = pigeonVar_list[0] as SingboxProxyRuntimeStatus + val endpoints = pigeonVar_list[1] as List + val message = pigeonVar_list[2] as String? + return SingboxProxyRuntimeState(status, endpoints, message) + } + } + fun toList(): List { + return listOf( + status, + endpoints, + message, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as SingboxProxyRuntimeState + return SingboxProxyApiPigeonUtils.deepEquals(this.status, other.status) && SingboxProxyApiPigeonUtils.deepEquals(this.endpoints, other.endpoints) && SingboxProxyApiPigeonUtils.deepEquals(this.message, other.message) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.status) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.endpoints) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.message) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SingboxProxyConfigResult ( + val configJson: String, + val endpoints: List +) + { + companion object { + fun fromList(pigeonVar_list: List): SingboxProxyConfigResult { + val configJson = pigeonVar_list[0] as String + val endpoints = pigeonVar_list[1] as List + return SingboxProxyConfigResult(configJson, endpoints) + } + } + fun toList(): List { + return listOf( + configJson, + endpoints, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as SingboxProxyConfigResult + return SingboxProxyApiPigeonUtils.deepEquals(this.configJson, other.configJson) && SingboxProxyApiPigeonUtils.deepEquals(this.endpoints, other.endpoints) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.configJson) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.endpoints) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SingboxProxyLogMessage ( + val level: String, + val message: String, + val timestamp: Long, + val profileId: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): SingboxProxyLogMessage { + val level = pigeonVar_list[0] as String + val message = pigeonVar_list[1] as String + val timestamp = pigeonVar_list[2] as Long + val profileId = pigeonVar_list[3] as String? + return SingboxProxyLogMessage(level, message, timestamp, profileId) + } + } + fun toList(): List { + return listOf( + level, + message, + timestamp, + profileId, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as SingboxProxyLogMessage + return SingboxProxyApiPigeonUtils.deepEquals(this.level, other.level) && SingboxProxyApiPigeonUtils.deepEquals(this.message, other.message) && SingboxProxyApiPigeonUtils.deepEquals(this.timestamp, other.timestamp) && SingboxProxyApiPigeonUtils.deepEquals(this.profileId, other.profileId) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.level) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.message) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.timestamp) + result = 31 * result + SingboxProxyApiPigeonUtils.deepHash(this.profileId) + return result + } +} +private open class SingboxProxyApiPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + SingboxProxyProfileType.ofRaw(it.toInt()) + } + } + 130.toByte() -> { + return (readValue(buffer) as Long?)?.let { + SingboxProxyRuntimeStatus.ofRaw(it.toInt()) + } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + SingboxProxyProfile.fromList(it) + } + } + 132.toByte() -> { + return (readValue(buffer) as? List)?.let { + SingboxProxyRuntimeOptions.fromList(it) + } + } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { + SingboxProxyDnsServerConfig.fromList(it) + } + } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + SingboxProxyDnsConfig.fromList(it) + } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { + SingboxProxyRuntimeEndpoint.fromList(it) + } + } + 136.toByte() -> { + return (readValue(buffer) as? List)?.let { + SingboxProxyRuntimeState.fromList(it) + } + } + 137.toByte() -> { + return (readValue(buffer) as? List)?.let { + SingboxProxyConfigResult.fromList(it) + } + } + 138.toByte() -> { + return (readValue(buffer) as? List)?.let { + SingboxProxyLogMessage.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is SingboxProxyProfileType -> { + stream.write(129) + writeValue(stream, value.raw.toLong()) + } + is SingboxProxyRuntimeStatus -> { + stream.write(130) + writeValue(stream, value.raw.toLong()) + } + is SingboxProxyProfile -> { + stream.write(131) + writeValue(stream, value.toList()) + } + is SingboxProxyRuntimeOptions -> { + stream.write(132) + writeValue(stream, value.toList()) + } + is SingboxProxyDnsServerConfig -> { + stream.write(133) + writeValue(stream, value.toList()) + } + is SingboxProxyDnsConfig -> { + stream.write(134) + writeValue(stream, value.toList()) + } + is SingboxProxyRuntimeEndpoint -> { + stream.write(135) + writeValue(stream, value.toList()) + } + is SingboxProxyRuntimeState -> { + stream.write(136) + writeValue(stream, value.toList()) + } + is SingboxProxyConfigResult -> { + stream.write(137) + writeValue(stream, value.toList()) + } + is SingboxProxyLogMessage -> { + stream.write(138) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface SingboxProxyApi { + fun validateProfile(profile: SingboxProxyProfile, callback: (Result) -> Unit) + fun buildConfig(profiles: List, options: SingboxProxyRuntimeOptions, callback: (Result) -> Unit) + fun start(profiles: List, options: SingboxProxyRuntimeOptions, callback: (Result) -> Unit) + fun stop(profileIds: List, callback: (Result) -> Unit) + fun stopAll(callback: (Result) -> Unit) + fun getState(): SingboxProxyRuntimeState + + companion object { + /** The codec used by SingboxProxyApi. */ + val codec: MessageCodec by lazy { + SingboxProxyApiPigeonCodec() + } + /** Sets up an instance of `SingboxProxyApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: SingboxProxyApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.validateProfile$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val profileArg = args[0] as SingboxProxyProfile + api.validateProfile(profileArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(SingboxProxyApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(SingboxProxyApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.buildConfig$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val profilesArg = args[0] as List + val optionsArg = args[1] as SingboxProxyRuntimeOptions + api.buildConfig(profilesArg, optionsArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(SingboxProxyApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(SingboxProxyApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.start$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val profilesArg = args[0] as List + val optionsArg = args[1] as SingboxProxyRuntimeOptions + api.start(profilesArg, optionsArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(SingboxProxyApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(SingboxProxyApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.stop$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val profileIdsArg = args[0] as List + api.stop(profileIdsArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(SingboxProxyApiPigeonUtils.wrapError(error)) + } else { + reply.reply(SingboxProxyApiPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.stopAll$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.stopAll{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(SingboxProxyApiPigeonUtils.wrapError(error)) + } else { + reply.reply(SingboxProxyApiPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.getState$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getState()) + } catch (exception: Throwable) { + SingboxProxyApiPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class SingboxProxyEventsApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by SingboxProxyEventsApi. */ + val codec: MessageCodec by lazy { + SingboxProxyApiPigeonCodec() + } + } + fun onStateChanged(stateArg: SingboxProxyRuntimeState, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyEventsApi.onStateChanged$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(stateArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(SingboxProxyApiPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onLogMessage(messageArg: SingboxProxyLogMessage, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyEventsApi.onLogMessage$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(messageArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(SingboxProxyApiPigeonUtils.createConnectionError(channelName))) + } + } + } +} diff --git a/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/FlutterSingboxProxyPluginTest.kt b/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/FlutterSingboxProxyPluginTest.kt new file mode 100644 index 00000000..c27331e6 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/FlutterSingboxProxyPluginTest.kt @@ -0,0 +1,247 @@ +package eu.weblibre.flutter_singbox_proxy + +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyProfile +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyProfileType +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyDnsConfig +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyDnsServerConfig +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeOptions +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import org.json.JSONObject + +internal class FlutterSingboxProxyPluginTest { + @Test + fun buildConfig_wrapsProfileOutboundWithAuthenticatedSocksInbound() { + val builder = SingboxConfigBuilder() + val profile = SingboxProxyProfile( + id = "wg-home", + name = "WireGuard Home", + type = SingboxProxyProfileType.WIREGUARD, + configJson = "{\"server\":\"example.test\"}", + secretJson = "{\"private_key\":\"secret\"}" + ) + + val result = builder.build( + listOf(profile), + SingboxProxyRuntimeOptions(preferredBasePort = 12500, blockUnmatchedTraffic = true) + ) + + assertEquals(1, result.endpoints.size) + assertEquals(12500, result.endpoints.single().port) + assertContains(result.configJson, "\"type\": \"socks\"") + assertContains(result.configJson, "\"type\": \"wireguard\"") + assertContains(result.configJson, "\"endpoints\"") + assertContains(result.configJson, "\"private_key\": \"secret\"") + } + + @Test + fun buildConfig_migratesWireGuardOutboundProfileToEndpoint() { + val builder = SingboxConfigBuilder() + val profile = SingboxProxyProfile( + id = "wg-home", + name = "WireGuard Home", + type = SingboxProxyProfileType.WIREGUARD, + configJson = """ + { + "type": "wireguard", + "server": "example.test", + "server_port": 51820, + "local_address": ["10.7.0.2/32"], + "peer_public_key": "peer", + "mtu": 1408 + } + """.trimIndent(), + secretJson = """ + { + "private_key": "secret", + "pre_shared_key": "psk" + } + """.trimIndent() + ) + + val result = builder.build( + listOf(profile), + SingboxProxyRuntimeOptions(preferredBasePort = 12500, blockUnmatchedTraffic = true) + ) + + assertContains(result.configJson, "\"endpoints\"") + assertContains(result.configJson, "\"address\": \"example.test\"") + assertContains(result.configJson, "\"port\": 51820") + assertContains(result.configJson, "\"address\": [") + assertContains(result.configJson, "\"public_key\": \"peer\"") + assertContains(result.configJson, "\"allowed_ips\": [") + } + + @Test + fun validateProfile_acceptsTypeSpecificConfigWithoutExplicitType() { + val builder = SingboxConfigBuilder() + val profile = SingboxProxyProfile( + id = "ss-main", + name = "Shadowsocks", + type = SingboxProxyProfileType.SHADOWSOCKS, + configJson = "{\"server\":\"example.test\"}", + secretJson = null + ) + + assertNull(builder.validateProfile(profile)) + } + + @Test + fun buildConfig_emitsLocalBootstrapAndDomainResolverForHostnames() { + val builder = SingboxConfigBuilder() + val profile = SingboxProxyProfile( + id = "profile-1", + name = "SOCKS", + type = SingboxProxyProfileType.SOCKS, + configJson = "{\"server\":\"127.0.0.1\",\"server_port\":1080}", + secretJson = null + ) + + val result = builder.build( + listOf(profile), + SingboxProxyRuntimeOptions( + preferredBasePort = 12500, + blockUnmatchedTraffic = true, + bootstrapDohUrl = "https://dns.example/dns-query", + dnsConfig = SingboxProxyDnsConfig( + servers = listOf( + dnsServer( + tag = "corp", + address = "tls://dns.example", + detourTag = "out-profile-1", + matchInbounds = listOf("in-profile-1") + ) + ), + finalServerTag = null, + domainStrategy = "" + ) + ) + ) + + val config = JSONObject(result.configJson) + val dns = config.getJSONObject("dns") + val servers = dns.getJSONArray("servers") + val local = servers.getJSONObject(0) + val corp = servers.getJSONObject(1) + val rule = dns.getJSONArray("rules").getJSONObject(0) + + // `local` is always emitted; LocalDNSTransport bridge backs it at runtime. + assertEquals("local", local.getString("type")) + assertEquals("local", local.getString("tag")) + assertFalse(local.has("detour")) + + assertEquals("tls", corp.getString("type")) + assertEquals("dns.example", corp.getString("server")) + // Hostname target → bootstrap through `local`. + assertEquals("local", corp.getString("domain_resolver")) + assertEquals("out-profile-1", corp.getString("detour")) + // The original `tls.server_name` plumbing is gone; SNI is derived + // from the preserved hostname in `server`. + assertFalse(corp.has("tls")) + + assertEquals("route", rule.getString("action")) + assertEquals("corp", rule.getString("server")) + assertEquals("local", dns.getString("final")) + + // route.default_domain_resolver ties WG peers and other outbound + // hostnames into the same `local` bridge. + val route = config.getJSONObject("route") + assertEquals("local", route.getString("default_domain_resolver")) + } + + @Test + fun buildConfig_omitsDomainResolverForIpLiteralServers() { + val builder = SingboxConfigBuilder() + val profile = SingboxProxyProfile( + id = "profile-1", + name = "SOCKS", + type = SingboxProxyProfileType.SOCKS, + configJson = "{\"server\":\"127.0.0.1\",\"server_port\":1080}", + secretJson = null + ) + + val result = builder.build( + listOf(profile), + SingboxProxyRuntimeOptions( + preferredBasePort = 12500, + blockUnmatchedTraffic = true, + bootstrapDohUrl = "https://dns.example/dns-query", + dnsConfig = SingboxProxyDnsConfig( + servers = listOf( + dnsServer( + tag = "plain", + address = "udp://1.2.3.4", + matchInbounds = emptyList() + ) + ), + finalServerTag = "plain", + domainStrategy = "" + ) + ) + ) + + val dns = JSONObject(result.configJson).getJSONObject("dns") + // `local` is still emitted unconditionally as the bootstrap anchor. + assertEquals("local", dns.getJSONArray("servers").getJSONObject(0).getString("type")) + val plain = dns.getJSONArray("servers").getJSONObject(1) + assertEquals("udp", plain.getString("type")) + assertEquals("1.2.3.4", plain.getString("server")) + // IP literal: no domain_resolver needed. + assertFalse(plain.has("domain_resolver")) + assertFalse(plain.has("tls")) + } + + @Test + fun buildConfig_rejectsDnsConfigWithoutBootstrapDohUrl() { + val builder = SingboxConfigBuilder() + val profile = SingboxProxyProfile( + id = "profile-1", + name = "SOCKS", + type = SingboxProxyProfileType.SOCKS, + configJson = "{\"server\":\"127.0.0.1\",\"server_port\":1080}", + secretJson = null + ) + + val error = assertFailsWith { + builder.build( + listOf(profile), + SingboxProxyRuntimeOptions( + preferredBasePort = 12500, + blockUnmatchedTraffic = true, + dnsConfig = SingboxProxyDnsConfig( + servers = listOf( + dnsServer(tag = "plain", address = "udp://1.2.3.4") + ), + finalServerTag = "plain", + domainStrategy = "" + ) + ) + ) + } + + assertEquals( + "bootstrapDohUrl is required when dnsConfig is provided.", + error.message + ) + } +} + +private fun dnsServer( + tag: String, + address: String, + detourTag: String? = null, + matchDomainSuffixes: List = emptyList(), + matchInbounds: List = emptyList() +) = SingboxProxyDnsServerConfig( + tag = tag, + address = address, + detourTag = detourTag, + matchDomainSuffixes = matchDomainSuffixes, + matchGeosites = emptyList(), + matchOutbounds = emptyList(), + matchInbounds = matchInbounds +) diff --git a/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxRuntimeManagerTest.kt b/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxRuntimeManagerTest.kt new file mode 100644 index 00000000..350f5f21 --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxRuntimeManagerTest.kt @@ -0,0 +1,84 @@ +package eu.weblibre.flutter_singbox_proxy + +import android.content.Context +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyProfile +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyProfileType +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeOptions +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeState +import eu.weblibre.flutter_singbox_proxy.generated.SingboxProxyRuntimeStatus +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue +import org.mockito.Mockito.mock + +internal class SingboxRuntimeManagerTest { + @Test + fun startFailure_preservesPreviouslyRunningEndpoints() { + val runtime = FakeLibboxRuntime(failOnStartAttempt = 2) + val manager = SingboxRuntimeManager( + context = mock(Context::class.java), + libboxRuntime = runtime, + dispatchToMain = { action -> action() } + ) + + val firstState = manager.awaitStart(listOf(profile(id = "profile-a"))).getOrThrow() + val failedResult = manager.awaitStart(listOf(profile(id = "profile-b"))) + val stateAfterFailure = manager.getState() + manager.close() + + assertTrue(failedResult.isFailure) + assertIs(failedResult.exceptionOrNull()) + assertEquals(SingboxProxyRuntimeStatus.ERROR, stateAfterFailure.status) + assertEquals(firstState.endpoints, stateAfterFailure.endpoints) + assertEquals("start failed", stateAfterFailure.message) + } +} + +private fun profile(id: String) = SingboxProxyProfile( + id = id, + name = id, + type = SingboxProxyProfileType.SOCKS, + configJson = """{"server":"127.0.0.1","server_port":1080}""", + secretJson = null +) + +private fun SingboxRuntimeManager.awaitStart( + profiles: List +): Result { + val latch = CountDownLatch(1) + var result: Result? = null + + start(profiles, SingboxProxyRuntimeOptions(preferredBasePort = 12080, blockUnmatchedTraffic = true)) { + result = it + latch.countDown() + } + + assertTrue(latch.await(5, TimeUnit.SECONDS), "Timed out waiting for start callback") + return result!! +} + +private class FakeLibboxRuntime( + private val failOnStartAttempt: Int, +) : LibboxRuntime(mock(Context::class.java)) { + private var startAttempts = 0 + + override fun isAvailable(): Boolean = true + + override fun start(configJson: String) { + startAttempts += 1 + if (startAttempts == failOnStartAttempt) { + throw IllegalStateException("start failed") + } + } + + override fun stopService() {} + + override fun close() {} + + override fun setLogSink(sink: ((Int, String) -> Unit)?) {} + + override fun setBootstrapDohUrl(url: String?) {} +} diff --git a/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxTagFormatTest.kt b/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxTagFormatTest.kt new file mode 100644 index 00000000..b210a68f --- /dev/null +++ b/packages/flutter_singbox_proxy/android/src/test/kotlin/eu/weblibre/flutter_singbox_proxy/SingboxTagFormatTest.kt @@ -0,0 +1,39 @@ +package eu.weblibre.flutter_singbox_proxy + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Tag format is part of the Dart↔Kotlin contract: the Dart DNS resolver in + * `dns_config_resolver.dart` emits `out-`/`in-` tags that must match what the + * config builder writes. The mirrored Dart test lives in + * `apps/weblibre/test/features/proxy/domain/services/singbox_tag_format_test.dart` + * — both must update together if this format ever changes. + */ +internal class SingboxTagFormatTest { + @Test + fun outboundTag_isPrefixedAndSanitized() { + assertEquals("out-singbox_foo-bar", SingboxTagFormat.outboundTag("singbox:foo-bar")) + } + + @Test + fun inboundTag_isPrefixedAndSanitized() { + assertEquals("in-singbox_foo-bar", SingboxTagFormat.inboundTag("singbox:foo-bar")) + } + + @Test + fun sanitizeTag_preservesAlphanumericDotsDashesUnderscores() { + assertEquals( + "Abc_123.x-Y", + SingboxTagFormat.sanitizeTag("Abc_123.x-Y") + ) + } + + @Test + fun sanitizeTag_replacesSpacesSlashesAndColons() { + assertEquals( + "a_b_c_d_e", + SingboxTagFormat.sanitizeTag("a:b c/d\\e") + ) + } +} diff --git a/packages/flutter_singbox_proxy/example/.gitignore b/packages/flutter_singbox_proxy/example/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/packages/flutter_singbox_proxy/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/flutter_singbox_proxy/example/README.md b/packages/flutter_singbox_proxy/example/README.md new file mode 100644 index 00000000..c4ba1ae6 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/README.md @@ -0,0 +1,17 @@ +# flutter_singbox_proxy_example + +Demonstrates how to use the flutter_singbox_proxy plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/flutter_singbox_proxy/example/analysis_options.yaml b/packages/flutter_singbox_proxy/example/analysis_options.yaml new file mode 100644 index 00000000..befa4622 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/analysis_options.yaml @@ -0,0 +1,4 @@ +# Analysis options for flutter_singbox_proxy example +# Includes root configuration from monorepo + +include: ../../../analysis_options.yaml diff --git a/packages/flutter_singbox_proxy/example/android/.gitignore b/packages/flutter_singbox_proxy/example/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/flutter_singbox_proxy/example/android/app/build.gradle b/packages/flutter_singbox_proxy/example/android/app/build.gradle new file mode 100644 index 00000000..17e40e1c --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/build.gradle @@ -0,0 +1,38 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +android { + namespace = "eu.weblibre.flutter_singbox_proxy_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + defaultConfig { + applicationId = "eu.weblibre.flutter_singbox_proxy_example" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + signingConfig = signingConfigs.debug + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/flutter_singbox_proxy/example/android/app/src/debug/AndroidManifest.xml b/packages/flutter_singbox_proxy/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/AndroidManifest.xml b/packages/flutter_singbox_proxy/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d69139b7 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/kotlin/eu/weblibre/flutter_singbox_proxy_example/MainActivity.kt b/packages/flutter_singbox_proxy/example/android/app/src/main/kotlin/eu/weblibre/flutter_singbox_proxy_example/MainActivity.kt new file mode 100644 index 00000000..f6c4ab87 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/src/main/kotlin/eu/weblibre/flutter_singbox_proxy_example/MainActivity.kt @@ -0,0 +1,5 @@ +package eu.weblibre.flutter_singbox_proxy_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/flutter_singbox_proxy/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/res/drawable/launch_background.xml b/packages/flutter_singbox_proxy/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/flutter_singbox_proxy/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/res/values-night/styles.xml b/packages/flutter_singbox_proxy/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/flutter_singbox_proxy/example/android/app/src/main/res/values/styles.xml b/packages/flutter_singbox_proxy/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/flutter_singbox_proxy/example/android/app/src/profile/AndroidManifest.xml b/packages/flutter_singbox_proxy/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/flutter_singbox_proxy/example/android/build.gradle b/packages/flutter_singbox_proxy/example/android/build.gradle new file mode 100644 index 00000000..3c76c793 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/packages/flutter_singbox_proxy/example/android/gradle.properties b/packages/flutter_singbox_proxy/example/android/gradle.properties new file mode 100644 index 00000000..fbee1d8c --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/packages/flutter_singbox_proxy/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_singbox_proxy/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..0922d266 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip diff --git a/packages/flutter_singbox_proxy/example/android/settings.gradle b/packages/flutter_singbox_proxy/example/android/settings.gradle new file mode 100644 index 00000000..b444b190 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.13.2" apply false + id "org.jetbrains.kotlin.android" version "2.3.21" apply false +} + +include ":app" diff --git a/packages/flutter_singbox_proxy/example/integration_test/plugin_integration_test.dart b/packages/flutter_singbox_proxy/example/integration_test/plugin_integration_test.dart new file mode 100644 index 00000000..35390277 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/integration_test/plugin_integration_test.dart @@ -0,0 +1,23 @@ +// This is a basic Flutter integration test. +// +// Since integration tests run in a full Flutter application, they can interact +// with the host side of a plugin implementation, unlike Dart unit tests. +// +// For more information about Flutter integration tests, please see +// https://flutter.dev/to/integration-testing + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getState test', (WidgetTester tester) async { + final FlutterSingboxProxy plugin = FlutterSingboxProxy(); + final state = await plugin.getState(); + + expect(state.status, SingboxProxyRuntimeStatus.stopped); + }); +} diff --git a/packages/flutter_singbox_proxy/example/lib/main.dart b/packages/flutter_singbox_proxy/example/lib/main.dart new file mode 100644 index 00000000..149c6879 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/lib/main.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + final proxy = FlutterSingboxProxy(); + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Plugin example app')), + body: FutureBuilder( + future: proxy.getState(), + builder: (context, snapshot) { + final status = snapshot.data?.status.name ?? 'unknown'; + return Center(child: Text('sing-box proxy status: $status')); + }, + ), + ), + ); + } +} diff --git a/packages/flutter_singbox_proxy/example/pubspec.yaml b/packages/flutter_singbox_proxy/example/pubspec.yaml new file mode 100644 index 00000000..136890e0 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/pubspec.yaml @@ -0,0 +1,86 @@ +name: flutter_singbox_proxy_example +description: "Demonstrates how to use the flutter_singbox_proxy plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev +resolution: workspace + +environment: + sdk: ^3.10.4 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + flutter_singbox_proxy: + # When depending on this package from a real application you should use: + # flutter_singbox_proxy: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/packages/flutter_singbox_proxy/example/test/widget_test.dart b/packages/flutter_singbox_proxy/example/test/widget_test.dart new file mode 100644 index 00000000..ee8663d8 --- /dev/null +++ b/packages/flutter_singbox_proxy/example/test/widget_test.dart @@ -0,0 +1,28 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_singbox_proxy_example/main.dart'; + +void main() { + testWidgets('shows proxy runtime status label', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Without a native messenger in widget tests the future remains pending, + // but the static label should still be present. + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.startsWith('sing-box proxy status:'), + ), + findsOneWidget, + ); + }); +} diff --git a/packages/flutter_singbox_proxy/lib/flutter_singbox_proxy.dart b/packages/flutter_singbox_proxy/lib/flutter_singbox_proxy.dart new file mode 100644 index 00000000..d57a1cfc --- /dev/null +++ b/packages/flutter_singbox_proxy/lib/flutter_singbox_proxy.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'src/singbox_proxy_api.g.dart'; + +export 'src/singbox_proxy_api.g.dart' + show + SingboxProxyConfigResult, + SingboxProxyDnsConfig, + SingboxProxyDnsServerConfig, + SingboxProxyLogMessage, + SingboxProxyProfile, + SingboxProxyProfileType, + SingboxProxyRuntimeEndpoint, + SingboxProxyRuntimeOptions, + SingboxProxyRuntimeState, + SingboxProxyRuntimeStatus; + +class FlutterSingboxProxy implements SingboxProxyEventsApi { + FlutterSingboxProxy({SingboxProxyApi? api}) + : _api = api ?? SingboxProxyApi() { + SingboxProxyEventsApi.setUp(this); + } + + final SingboxProxyApi _api; + final _stateController = + StreamController.broadcast(); + final _logController = StreamController.broadcast(); + + Stream get stateStream => _stateController.stream; + Stream get logStream => _logController.stream; + + Future validateProfile(SingboxProxyProfile profile) { + return _api.validateProfile(profile); + } + + Future buildConfig( + List profiles, { + SingboxProxyRuntimeOptions? options, + }) { + return _api.buildConfig(profiles, options ?? SingboxProxyRuntimeOptions()); + } + + Future start( + List profiles, { + SingboxProxyRuntimeOptions? options, + }) { + return _api.start(profiles, options ?? SingboxProxyRuntimeOptions()); + } + + Future stop(List profileIds) => _api.stop(profileIds); + + Future stopAll() => _api.stopAll(); + + Future getState() => _api.getState(); + + @override + void onStateChanged(SingboxProxyRuntimeState state) { + _stateController.add(state); + } + + @override + void onLogMessage(SingboxProxyLogMessage message) { + _logController.add(message); + } + + Future dispose() async { + SingboxProxyEventsApi.setUp(null); + await _stateController.close(); + await _logController.close(); + } +} diff --git a/packages/flutter_singbox_proxy/lib/src/singbox_proxy_api.g.dart b/packages/flutter_singbox_proxy/lib/src/singbox_proxy_api.g.dart new file mode 100644 index 00000000..12bd2b10 --- /dev/null +++ b/packages/flutter_singbox_proxy/lib/src/singbox_proxy_api.g.dart @@ -0,0 +1,912 @@ +// Autogenerated from Pigeon (v26.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; +} + +List wrapResponse({ + Object? result, + PlatformException? error, + bool empty = false, +}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == b; + } + if (a is List && b is List) { + return a.length == b.length && + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); + } + if (a is Map && b is Map) { + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; + } + return a == b; +} + +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + +enum SingboxProxyProfileType { + socks, + http, + shadowsocks, + vmess, + vless, + trojan, + naive, + hysteria, + hysteria2, + tuic, + ssh, + wireguard, + shadowTls, + anyTls, + customOutbound, +} + +enum SingboxProxyRuntimeStatus { stopped, starting, running, stopping, error } + +class SingboxProxyProfile { + SingboxProxyProfile({ + required this.id, + required this.name, + required this.type, + required this.configJson, + this.secretJson, + }); + + String id; + + String name; + + SingboxProxyProfileType type; + + /// Public profile configuration as JSON. The schema is intentionally owned by + /// the profile type so the Pigeon API stays stable while sing-box evolves. + String configJson; + + /// Resolved secret values as JSON. Flutter stores secrets independently and + /// only passes them to native code when building or starting a runtime config. + String? secretJson; + + List _toList() { + return [id, name, type, configJson, secretJson]; + } + + Object encode() { + return _toList(); + } + + static SingboxProxyProfile decode(Object result) { + result as List; + return SingboxProxyProfile( + id: result[0]! as String, + name: result[1]! as String, + type: result[2]! as SingboxProxyProfileType, + configJson: result[3]! as String, + secretJson: result[4] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SingboxProxyProfile || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(id, other.id) && + _deepEquals(name, other.name) && + _deepEquals(type, other.type) && + _deepEquals(configJson, other.configJson) && + _deepEquals(secretJson, other.secretJson); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class SingboxProxyRuntimeOptions { + SingboxProxyRuntimeOptions({ + this.preferredBasePort, + this.blockUnmatchedTraffic = true, + this.dnsConfig, + this.bootstrapDohUrl, + }); + + /// Optional preferred start port for generated local SOCKS inbounds. + int? preferredBasePort; + + /// If true, traffic entering sing-box without a matching inbound rule is + /// rejected instead of falling through to direct. + bool blockUnmatchedTraffic; + + /// Optional DNS block emitted into sing-box config. When null, sing-box + /// uses its built-in default (system resolver), which can leak DNS outside + /// the proxy — callers should always provide an explicit configuration. + SingboxProxyDnsConfig? dnsConfig; + + /// DoH endpoint used by the native LocalDNSTransport bridge to bootstrap + /// hostname-only DNS server addresses (and any other hostname appearing in + /// the sing-box config). When null, the bridge refuses to resolve and + /// sing-box's stock `/etc/resolv.conf`/127.0.0.1:53 path runs — which is + /// broken on Android. Callers should always pass the browser DoH URL. + String? bootstrapDohUrl; + + List _toList() { + return [ + preferredBasePort, + blockUnmatchedTraffic, + dnsConfig, + bootstrapDohUrl, + ]; + } + + Object encode() { + return _toList(); + } + + static SingboxProxyRuntimeOptions decode(Object result) { + result as List; + return SingboxProxyRuntimeOptions( + preferredBasePort: result[0] as int?, + blockUnmatchedTraffic: result[1]! as bool, + dnsConfig: result[2] as SingboxProxyDnsConfig?, + bootstrapDohUrl: result[3] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SingboxProxyRuntimeOptions || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(preferredBasePort, other.preferredBasePort) && + _deepEquals(blockUnmatchedTraffic, other.blockUnmatchedTraffic) && + _deepEquals(dnsConfig, other.dnsConfig) && + _deepEquals(bootstrapDohUrl, other.bootstrapDohUrl); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class SingboxProxyDnsServerConfig { + SingboxProxyDnsServerConfig({ + required this.tag, + required this.address, + this.detourTag, + this.matchDomainSuffixes = const [], + this.matchGeosites = const [], + this.matchOutbounds = const [], + this.matchInbounds = const [], + }); + + /// sing-box server tag, used to reference the server from `dns.rules`. + String tag; + + /// Server address. `https://...`, `tls://...`, `quic://...`, or plain IP. + /// Hostnames are resolved on demand via the platform LocalDNSTransport + /// bridge (sing-box `type: "local"` with our DoH-backed implementation). + String address; + + /// Outbound tag to dial the resolver through, or `direct` for direct, or + /// null when sing-box should pick automatically. + String? detourTag; + + /// If non-empty, attaches a `dns.rules` entry routing matching domains to + /// this server. + List matchDomainSuffixes; + + /// Advanced sing-box geosite selectors (e.g. `geosite:cn`). + List matchGeosites; + + /// If non-empty, attaches a `dns.rules` entry routing queries that *the + /// listed outbounds* originate to this server. Note: sing-box treats an + /// outbound's own bootstrap lookups (e.g. WireGuard peer hostname + /// resolution) as queries from that outbound, so using this for per-profile + /// scoping creates a chicken-and-egg loop at startup. Prefer + /// [matchInbounds] for "queries from tabs routed through this profile". + List matchOutbounds; + + /// If non-empty, attaches a `dns.rules` entry matching the listed inbound + /// tags. Queries entering via that inbound (e.g. a tab whose container is + /// bound to this profile's local SOCKS inbound) resolve through this + /// server. Endpoint-bootstrap lookups don't come from any inbound, so this + /// scope safely excludes them. + List matchInbounds; + + List _toList() { + return [ + tag, + address, + detourTag, + matchDomainSuffixes, + matchGeosites, + matchOutbounds, + matchInbounds, + ]; + } + + Object encode() { + return _toList(); + } + + static SingboxProxyDnsServerConfig decode(Object result) { + result as List; + return SingboxProxyDnsServerConfig( + tag: result[0]! as String, + address: result[1]! as String, + detourTag: result[2] as String?, + matchDomainSuffixes: (result[3]! as List).cast(), + matchGeosites: (result[4]! as List).cast(), + matchOutbounds: (result[5]! as List).cast(), + matchInbounds: (result[6]! as List).cast(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SingboxProxyDnsServerConfig || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(tag, other.tag) && + _deepEquals(address, other.address) && + _deepEquals(detourTag, other.detourTag) && + _deepEquals(matchDomainSuffixes, other.matchDomainSuffixes) && + _deepEquals(matchGeosites, other.matchGeosites) && + _deepEquals(matchOutbounds, other.matchOutbounds) && + _deepEquals(matchInbounds, other.matchInbounds); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class SingboxProxyDnsConfig { + SingboxProxyDnsConfig({ + required this.servers, + this.finalServerTag, + required this.domainStrategy, + }); + + List servers; + + /// Server tag used as `dns.final`. When null, sing-box uses the first + /// server in the list as the fallback. + String? finalServerTag; + + /// sing-box `dns.strategy` string. e.g. `prefer_ipv4`, `ipv4_only`. + String domainStrategy; + + List _toList() { + return [servers, finalServerTag, domainStrategy]; + } + + Object encode() { + return _toList(); + } + + static SingboxProxyDnsConfig decode(Object result) { + result as List; + return SingboxProxyDnsConfig( + servers: (result[0]! as List) + .cast(), + finalServerTag: result[1] as String?, + domainStrategy: result[2]! as String, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SingboxProxyDnsConfig || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(servers, other.servers) && + _deepEquals(finalServerTag, other.finalServerTag) && + _deepEquals(domainStrategy, other.domainStrategy); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class SingboxProxyRuntimeEndpoint { + SingboxProxyRuntimeEndpoint({ + required this.profileId, + required this.host, + required this.port, + required this.username, + required this.password, + }); + + String profileId; + + String host; + + int port; + + String username; + + String password; + + List _toList() { + return [profileId, host, port, username, password]; + } + + Object encode() { + return _toList(); + } + + static SingboxProxyRuntimeEndpoint decode(Object result) { + result as List; + return SingboxProxyRuntimeEndpoint( + profileId: result[0]! as String, + host: result[1]! as String, + port: result[2]! as int, + username: result[3]! as String, + password: result[4]! as String, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SingboxProxyRuntimeEndpoint || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(profileId, other.profileId) && + _deepEquals(host, other.host) && + _deepEquals(port, other.port) && + _deepEquals(username, other.username) && + _deepEquals(password, other.password); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class SingboxProxyRuntimeState { + SingboxProxyRuntimeState({ + required this.status, + required this.endpoints, + this.message, + }); + + SingboxProxyRuntimeStatus status; + + List endpoints; + + String? message; + + List _toList() { + return [status, endpoints, message]; + } + + Object encode() { + return _toList(); + } + + static SingboxProxyRuntimeState decode(Object result) { + result as List; + return SingboxProxyRuntimeState( + status: result[0]! as SingboxProxyRuntimeStatus, + endpoints: (result[1]! as List) + .cast(), + message: result[2] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SingboxProxyRuntimeState || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(status, other.status) && + _deepEquals(endpoints, other.endpoints) && + _deepEquals(message, other.message); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class SingboxProxyConfigResult { + SingboxProxyConfigResult({required this.configJson, required this.endpoints}); + + String configJson; + + List endpoints; + + List _toList() { + return [configJson, endpoints]; + } + + Object encode() { + return _toList(); + } + + static SingboxProxyConfigResult decode(Object result) { + result as List; + return SingboxProxyConfigResult( + configJson: result[0]! as String, + endpoints: (result[1]! as List) + .cast(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SingboxProxyConfigResult || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(configJson, other.configJson) && + _deepEquals(endpoints, other.endpoints); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class SingboxProxyLogMessage { + SingboxProxyLogMessage({ + required this.level, + required this.message, + required this.timestamp, + this.profileId, + }); + + String level; + + String message; + + int timestamp; + + String? profileId; + + List _toList() { + return [level, message, timestamp, profileId]; + } + + Object encode() { + return _toList(); + } + + static SingboxProxyLogMessage decode(Object result) { + result as List; + return SingboxProxyLogMessage( + level: result[0]! as String, + message: result[1]! as String, + timestamp: result[2]! as int, + profileId: result[3] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SingboxProxyLogMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(level, other.level) && + _deepEquals(message, other.message) && + _deepEquals(timestamp, other.timestamp) && + _deepEquals(profileId, other.profileId); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is SingboxProxyProfileType) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is SingboxProxyRuntimeStatus) { + buffer.putUint8(130); + writeValue(buffer, value.index); + } else if (value is SingboxProxyProfile) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is SingboxProxyRuntimeOptions) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is SingboxProxyDnsServerConfig) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is SingboxProxyDnsConfig) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is SingboxProxyRuntimeEndpoint) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is SingboxProxyRuntimeState) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is SingboxProxyConfigResult) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is SingboxProxyLogMessage) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final value = readValue(buffer) as int?; + return value == null ? null : SingboxProxyProfileType.values[value]; + case 130: + final value = readValue(buffer) as int?; + return value == null ? null : SingboxProxyRuntimeStatus.values[value]; + case 131: + return SingboxProxyProfile.decode(readValue(buffer)!); + case 132: + return SingboxProxyRuntimeOptions.decode(readValue(buffer)!); + case 133: + return SingboxProxyDnsServerConfig.decode(readValue(buffer)!); + case 134: + return SingboxProxyDnsConfig.decode(readValue(buffer)!); + case 135: + return SingboxProxyRuntimeEndpoint.decode(readValue(buffer)!); + case 136: + return SingboxProxyRuntimeState.decode(readValue(buffer)!); + case 137: + return SingboxProxyConfigResult.decode(readValue(buffer)!); + case 138: + return SingboxProxyLogMessage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class SingboxProxyApi { + /// Constructor for [SingboxProxyApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + SingboxProxyApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future validateProfile(SingboxProxyProfile profile) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.validateProfile$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [profile], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + return pigeonVar_replyValue as String?; + } + + Future buildConfig( + List profiles, + SingboxProxyRuntimeOptions options, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.buildConfig$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [profiles, options], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as SingboxProxyConfigResult; + } + + Future start( + List profiles, + SingboxProxyRuntimeOptions options, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.start$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [profiles, options], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as SingboxProxyRuntimeState; + } + + Future stop(List profileIds) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.stop$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [profileIds], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future stopAll() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.stopAll$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future getState() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.getState$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as SingboxProxyRuntimeState; + } +} + +abstract class SingboxProxyEventsApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + void onStateChanged(SingboxProxyRuntimeState state); + + void onLogMessage(SingboxProxyLogMessage message); + + static void setUp( + SingboxProxyEventsApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyEventsApi.onStateChanged$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final SingboxProxyRuntimeState arg_state = + args[0]! as SingboxProxyRuntimeState; + try { + api.onStateChanged(arg_state); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyEventsApi.onLogMessage$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final SingboxProxyLogMessage arg_message = + args[0]! as SingboxProxyLogMessage; + try { + api.onLogMessage(arg_message); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + } +} diff --git a/packages/flutter_singbox_proxy/pigeons/singbox_proxy_api.dart b/packages/flutter_singbox_proxy/pigeons/singbox_proxy_api.dart new file mode 100644 index 00000000..0ca5d040 --- /dev/null +++ b/packages/flutter_singbox_proxy/pigeons/singbox_proxy_api.dart @@ -0,0 +1,227 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartPackageName: 'flutter_singbox_proxy', + dartOut: 'lib/src/singbox_proxy_api.g.dart', + dartOptions: DartOptions(), + kotlinOut: + 'android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/generated/SingboxProxyApi.g.kt', + kotlinOptions: KotlinOptions( + package: 'eu.weblibre.flutter_singbox_proxy.generated', + ), + ), +) +enum SingboxProxyProfileType { + socks, + http, + shadowsocks, + vmess, + vless, + trojan, + naive, + hysteria, + hysteria2, + tuic, + ssh, + wireguard, + shadowTls, + anyTls, + customOutbound, +} + +enum SingboxProxyRuntimeStatus { stopped, starting, running, stopping, error } + +class SingboxProxyProfile { + SingboxProxyProfile({ + required this.id, + required this.name, + required this.type, + required this.configJson, + this.secretJson, + }); + + final String id; + final String name; + final SingboxProxyProfileType type; + + /// Public profile configuration as JSON. The schema is intentionally owned by + /// the profile type so the Pigeon API stays stable while sing-box evolves. + final String configJson; + + /// Resolved secret values as JSON. Flutter stores secrets independently and + /// only passes them to native code when building or starting a runtime config. + final String? secretJson; +} + +class SingboxProxyRuntimeOptions { + SingboxProxyRuntimeOptions({ + this.preferredBasePort, + this.blockUnmatchedTraffic = true, + this.dnsConfig, + this.bootstrapDohUrl, + }); + + /// Optional preferred start port for generated local SOCKS inbounds. + final int? preferredBasePort; + + /// If true, traffic entering sing-box without a matching inbound rule is + /// rejected instead of falling through to direct. + final bool blockUnmatchedTraffic; + + /// Optional DNS block emitted into sing-box config. When null, sing-box + /// uses its built-in default (system resolver), which can leak DNS outside + /// the proxy — callers should always provide an explicit configuration. + final SingboxProxyDnsConfig? dnsConfig; + + /// DoH endpoint used by the native LocalDNSTransport bridge to bootstrap + /// hostname-only DNS server addresses (and any other hostname appearing in + /// the sing-box config). When null, the bridge refuses to resolve and + /// sing-box's stock `/etc/resolv.conf`/127.0.0.1:53 path runs — which is + /// broken on Android. Callers should always pass the browser DoH URL. + final String? bootstrapDohUrl; +} + +class SingboxProxyDnsServerConfig { + SingboxProxyDnsServerConfig({ + required this.tag, + required this.address, + this.detourTag, + this.matchDomainSuffixes = const [], + this.matchGeosites = const [], + this.matchOutbounds = const [], + this.matchInbounds = const [], + }); + + /// sing-box server tag, used to reference the server from `dns.rules`. + final String tag; + + /// Server address. `https://...`, `tls://...`, `quic://...`, or plain IP. + /// Hostnames are resolved on demand via the platform LocalDNSTransport + /// bridge (sing-box `type: "local"` with our DoH-backed implementation). + final String address; + + /// Outbound tag to dial the resolver through, or `direct` for direct, or + /// null when sing-box should pick automatically. + final String? detourTag; + + /// If non-empty, attaches a `dns.rules` entry routing matching domains to + /// this server. + final List matchDomainSuffixes; + + /// Advanced sing-box geosite selectors (e.g. `geosite:cn`). + final List matchGeosites; + + /// If non-empty, attaches a `dns.rules` entry routing queries that *the + /// listed outbounds* originate to this server. Note: sing-box treats an + /// outbound's own bootstrap lookups (e.g. WireGuard peer hostname + /// resolution) as queries from that outbound, so using this for per-profile + /// scoping creates a chicken-and-egg loop at startup. Prefer + /// [matchInbounds] for "queries from tabs routed through this profile". + final List matchOutbounds; + + /// If non-empty, attaches a `dns.rules` entry matching the listed inbound + /// tags. Queries entering via that inbound (e.g. a tab whose container is + /// bound to this profile's local SOCKS inbound) resolve through this + /// server. Endpoint-bootstrap lookups don't come from any inbound, so this + /// scope safely excludes them. + final List matchInbounds; +} + +class SingboxProxyDnsConfig { + SingboxProxyDnsConfig({ + required this.servers, + this.finalServerTag, + required this.domainStrategy, + }); + + final List servers; + + /// Server tag used as `dns.final`. When null, sing-box uses the first + /// server in the list as the fallback. + final String? finalServerTag; + + /// sing-box `dns.strategy` string. e.g. `prefer_ipv4`, `ipv4_only`. + final String domainStrategy; +} + +class SingboxProxyRuntimeEndpoint { + SingboxProxyRuntimeEndpoint({ + required this.profileId, + required this.host, + required this.port, + required this.username, + required this.password, + }); + + final String profileId; + final String host; + final int port; + final String username; + final String password; +} + +class SingboxProxyRuntimeState { + SingboxProxyRuntimeState({ + required this.status, + required this.endpoints, + this.message, + }); + + final SingboxProxyRuntimeStatus status; + final List endpoints; + final String? message; +} + +class SingboxProxyConfigResult { + SingboxProxyConfigResult({required this.configJson, required this.endpoints}); + + final String configJson; + final List endpoints; +} + +class SingboxProxyLogMessage { + SingboxProxyLogMessage({ + required this.level, + required this.message, + required this.timestamp, + this.profileId, + }); + + final String level; + final String message; + final int timestamp; + final String? profileId; +} + +@HostApi() +abstract class SingboxProxyApi { + @async + String? validateProfile(SingboxProxyProfile profile); + + @async + SingboxProxyConfigResult buildConfig( + List profiles, + SingboxProxyRuntimeOptions options, + ); + + @async + SingboxProxyRuntimeState start( + List profiles, + SingboxProxyRuntimeOptions options, + ); + + @async + void stop(List profileIds); + + @async + void stopAll(); + + SingboxProxyRuntimeState getState(); +} + +@FlutterApi() +abstract class SingboxProxyEventsApi { + void onStateChanged(SingboxProxyRuntimeState state); + void onLogMessage(SingboxProxyLogMessage message); +} diff --git a/packages/flutter_singbox_proxy/pubspec.yaml b/packages/flutter_singbox_proxy/pubspec.yaml new file mode 100644 index 00000000..946fefa3 --- /dev/null +++ b/packages/flutter_singbox_proxy/pubspec.yaml @@ -0,0 +1,71 @@ +name: flutter_singbox_proxy +description: "Android sing-box proxy runtime plugin for WebLibre." +version: 0.0.1 +homepage: +resolution: workspace + +environment: + sdk: ^3.10.4 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + lint: ^2.8.0 + pigeon: ^26.3.2 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: eu.weblibre.flutter_singbox_proxy + pluginClass: FlutterSingboxProxyPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/packages/flutter_singbox_proxy/scripts/build-libbox-android.sh b/packages/flutter_singbox_proxy/scripts/build-libbox-android.sh new file mode 100755 index 00000000..aaacbe7a --- /dev/null +++ b/packages/flutter_singbox_proxy/scripts/build-libbox-android.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" +DEFAULT_SING_BOX_SOURCE="$(cd "$PACKAGE_DIR/../../.." && pwd -P)/sing-box" + +SING_BOX_SOURCE="${SING_BOX_SOURCE:-$DEFAULT_SING_BOX_SOURCE}" +OUTPUT_DIR="$PACKAGE_DIR/android/libs" +BUILD_DEBUG=0 +PLATFORM="" +INSTALL_GOMOBILE=1 + +usage() { + cat <<'EOF' +Usage: build-libbox-android.sh [options] + +Build official sing-box libbox Android AARs and copy them into this plugin. + +Options: + --source PATH sing-box source checkout (default: ../../../sing-box) + --output-dir PATH destination for libbox.aar (default: android/libs) + --platform TARGET gomobile target, e.g. android/arm64 for faster local builds + --debug build sing-box debug variant + --skip-install-gomobile require gomobile/gobind to already exist in GOPATH/bin + -h, --help show this help + +Environment: + SING_BOX_SOURCE same as --source + JAVA_HOME should point to OpenJDK 17; auto-detected locally when unset + ANDROID_HOME Android SDK path, required by sing-box build tooling +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --source) + SING_BOX_SOURCE="$2" + shift 2 + ;; + --output-dir) + OUTPUT_DIR="$2" + shift 2 + ;; + --platform) + PLATFORM="$2" + shift 2 + ;; + --debug) + BUILD_DEBUG=1 + shift + ;; + --skip-install-gomobile) + INSTALL_GOMOBILE=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ ! -f "$SING_BOX_SOURCE/Makefile" || ! -d "$SING_BOX_SOURCE/experimental/libbox" ]]; then + echo "sing-box source not found at: $SING_BOX_SOURCE" >&2 + echo "Pass --source PATH or set SING_BOX_SOURCE." >&2 + exit 1 +fi + +if [[ -z "${JAVA_HOME:-}" && -x /usr/lib/jvm/java-17-openjdk/bin/java ]]; then + export JAVA_HOME=/usr/lib/jvm/java-17-openjdk +fi + +JAVA_BIN="${JAVA_HOME:+$JAVA_HOME/bin/}java" +if ! "$JAVA_BIN" --version 2>/dev/null | grep -q 'openjdk 17'; then + echo "sing-box Android libbox build requires OpenJDK 17." >&2 + echo "Current java:" >&2 + "$JAVA_BIN" --version >&2 || true + exit 1 +fi + +if ! command -v go >/dev/null 2>&1; then + echo "go is required to build sing-box libbox." >&2 + exit 1 +fi + +GOPATH="${GOPATH:-$(go env GOPATH)}" +if [[ $INSTALL_GOMOBILE -eq 1 ]]; then + needs_gomobile_install=0 + if [[ ! -x "$GOPATH/bin/gomobile" || ! -x "$GOPATH/bin/gobind" ]]; then + needs_gomobile_install=1 + elif ! "$GOPATH/bin/gomobile" bind -h 2>&1 | grep -q -- '-libname'; then + needs_gomobile_install=1 + fi + + if [[ $needs_gomobile_install -eq 1 ]]; then + go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12 + go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12 + fi +fi + +mkdir -p "$OUTPUT_DIR" + +pushd "$SING_BOX_SOURCE" >/dev/null +args=(go run ./cmd/internal/build_libbox -target android) +if [[ $BUILD_DEBUG -eq 1 ]]; then + args+=(-debug) +fi +if [[ -n "$PLATFORM" ]]; then + args+=(-platform "$PLATFORM") +fi + +"${args[@]}" + +cp -f libbox.aar "$OUTPUT_DIR/libbox.aar" +if [[ -f libbox-legacy.aar ]]; then + cp -f libbox-legacy.aar "$OUTPUT_DIR/libbox-legacy.aar" +fi +popd >/dev/null + +echo "Installed $OUTPUT_DIR/libbox.aar" +if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$OUTPUT_DIR"/libbox*.aar +fi diff --git a/packages/flutter_singbox_proxy/scripts/fdroid-prebuild.sh b/packages/flutter_singbox_proxy/scripts/fdroid-prebuild.sh new file mode 100755 index 00000000..f52f4588 --- /dev/null +++ b/packages/flutter_singbox_proxy/scripts/fdroid-prebuild.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)" + +"$REPO_ROOT/native/go_mobile_runtime/scripts/fdroid-prebuild.sh" "$@" diff --git a/packages/flutter_singbox_proxy/test/flutter_singbox_proxy_test.dart b/packages/flutter_singbox_proxy/test/flutter_singbox_proxy_test.dart new file mode 100644 index 00000000..06aba1bf --- /dev/null +++ b/packages/flutter_singbox_proxy/test/flutter_singbox_proxy_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart'; + +void main() { + test('profile model keeps generic sing-box config boundary', () { + final profile = SingboxProxyProfile( + id: 'wg-home', + name: 'WireGuard Home', + type: SingboxProxyProfileType.wireguard, + configJson: '{"server":"example.test"}', + secretJson: '{"private_key":"secret"}', + ); + + expect(profile.id, 'wg-home'); + expect(profile.type, SingboxProxyProfileType.wireguard); + expect(profile.secretJson, contains('private_key')); + }); + + test('runtime options default to blocking unmatched traffic', () { + final options = SingboxProxyRuntimeOptions(); + + expect(options.preferredBasePort, isNull); + expect(options.blockUnmatchedTraffic, isTrue); + }); +} diff --git a/packages/flutter_tor/android/build.gradle b/packages/flutter_tor/android/build.gradle index e373f4e0..66fe29fb 100644 --- a/packages/flutter_tor/android/build.gradle +++ b/packages/flutter_tor/android/build.gradle @@ -22,6 +22,16 @@ allprojects { } } +def weblibreGoMobileAar = file("../../../native/go_mobile_runtime/build/weblibre-go.aar") +def requireWebLibreGoMobileAar = { + if (!weblibreGoMobileAar.exists()) { + throw new GradleException( + "Missing combined gomobile runtime AAR: ${weblibreGoMobileAar}. " + + "Run native/go_mobile_runtime/scripts/build-android.sh from the repository root." + ) + } +} + apply plugin: "com.android.library" apply plugin: "kotlin-android" @@ -53,8 +63,9 @@ android { implementation("info.guardianproject:tor-android:0.4.9.6") implementation("info.guardianproject:jtorctl:0.4.5.7") - // Pluggable transports - implementation("com.netzarchitekten:IPtProxy:5.4.1") + // Pluggable transports are provided by the app-level combined gomobile AAR. + requireWebLibreGoMobileAar() + compileOnly(files(weblibreGoMobileAar)) // Coroutines for async operations implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") diff --git a/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/FlutterTorPlugin.kt b/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/FlutterTorPlugin.kt index 6649d6fa..73e8e5ef 100644 --- a/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/FlutterTorPlugin.kt +++ b/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/FlutterTorPlugin.kt @@ -1,6 +1,5 @@ package eu.weblibre.flutter_tor -import IPtProxy.Controller import android.content.ComponentName import android.content.Context import android.content.Intent diff --git a/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/PluggableTransportManager.kt b/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/PluggableTransportManager.kt index 7624f36e..8da81523 100644 --- a/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/PluggableTransportManager.kt +++ b/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/PluggableTransportManager.kt @@ -2,9 +2,9 @@ package eu.weblibre.flutter_tor import android.content.Context import android.util.Log -import IPtProxy.Controller -import IPtProxy.IPtProxy -import IPtProxy.OnTransportEvents +import io.nekohasekai.IPtProxy.Controller +import io.nekohasekai.IPtProxy.IPtProxy +import io.nekohasekai.IPtProxy.OnTransportEvents import java.io.File /** diff --git a/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/ProxyImpl.kt b/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/ProxyImpl.kt index 9ee2559b..e6dafe6b 100644 --- a/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/ProxyImpl.kt +++ b/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/ProxyImpl.kt @@ -19,11 +19,12 @@ */ package eu.weblibre.flutter_tor -import IPtProxy.IPtProxy import eu.weblibre.flutter_tor.generated.IPtProxyController import eu.weblibre.flutter_tor.generated.TransportType +import io.nekohasekai.IPtProxy.Controller +import io.nekohasekai.IPtProxy.IPtProxy -class ProxyImpl(val controller: IPtProxy.Controller) : IPtProxyController { +class ProxyImpl(val controller: Controller) : IPtProxyController { override fun start(proxyType: TransportType, proxy: String): Long { val type = when (proxyType) { TransportType.SNOWFLAKE -> IPtProxy.Snowflake @@ -55,4 +56,4 @@ class ProxyImpl(val controller: IPtProxy.Controller) : IPtProxyController { controller.stop(type) } -} \ No newline at end of file +} diff --git a/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/TorConfig.kt b/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/TorConfig.kt index 87a635fb..acccbf4c 100644 --- a/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/TorConfig.kt +++ b/packages/flutter_tor/android/src/main/kotlin/eu/weblibre/flutter_tor/TorConfig.kt @@ -1,7 +1,7 @@ package eu.weblibre.flutter_tor import eu.weblibre.flutter_tor.generated.TorConfiguration -import IPtProxy.IPtProxy +import io.nekohasekai.IPtProxy.IPtProxy import java.io.File /** diff --git a/packages/flutter_tor/lib/src/tor_api.g.dart b/packages/flutter_tor/lib/src/tor_api.g.dart index 5192d6a0..753cf003 100644 --- a/packages/flutter_tor/lib/src/tor_api.g.dart +++ b/packages/flutter_tor/lib/src/tor_api.g.dart @@ -10,9 +10,9 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; Object? _extractReplyValueOrThrow( - List? replyList, - String channelName, { - required bool isNullValid, + List? replyList, + String channelName, { + required bool isNullValid, }) { if (replyList == null) { throw PlatformException( @@ -34,8 +34,11 @@ Object? _extractReplyValueOrThrow( return replyList.firstOrNull; } - -List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { +List wrapResponse({ + Object? result, + PlatformException? error, + bool empty = false, +}) { if (empty) { return []; } @@ -44,6 +47,7 @@ List wrapResponse({Object? result, PlatformException? error, bool empty } return [error.code, error.message, error.details]; } + bool _deepEquals(Object? a, Object? b) { if (identical(a, b)) { return true; @@ -56,8 +60,9 @@ bool _deepEquals(Object? a, Object? b) { } if (a is List && b is List) { return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); } if (a is Map && b is Map) { if (a.length != b.length) { @@ -106,23 +111,29 @@ int _deepHash(Object? value) { return value.hashCode; } - /// Transport types for Tor connections enum TransportType { /// Direct Tor connection (no bridges) none, + /// obfs4 pluggable transport obfs4, + /// Snowflake pluggable transport (default broker) snowflake, + /// Snowflake via AMP cache snowflakeAmp, + /// Meek pluggable transport meek, + /// Meek via Azure CDN meekAzure, + /// WebTunnel pluggable transport webtunnel, + /// Custom bridge lines (passthrough) custom, } @@ -163,7 +174,8 @@ class TorConfiguration { } Object encode() { - return _toList(); } + return _toList(); + } static TorConfiguration decode(Object result) { result as List; @@ -185,7 +197,11 @@ class TorConfiguration { if (identical(this, other)) { return true; } - return _deepEquals(transport, other.transport) && _deepEquals(bridgeLines, other.bridgeLines) && _deepEquals(entryNodeCountries, other.entryNodeCountries) && _deepEquals(exitNodeCountries, other.exitNodeCountries) && _deepEquals(strictNodes, other.strictNodes); + return _deepEquals(transport, other.transport) && + _deepEquals(bridgeLines, other.bridgeLines) && + _deepEquals(entryNodeCountries, other.entryNodeCountries) && + _deepEquals(exitNodeCountries, other.exitNodeCountries) && + _deepEquals(strictNodes, other.strictNodes); } @override @@ -229,7 +245,8 @@ class TorStatus { } Object encode() { - return _toList(); } + return _toList(); + } static TorStatus decode(Object result) { result as List; @@ -251,7 +268,11 @@ class TorStatus { if (identical(this, other)) { return true; } - return _deepEquals(isRunning, other.isRunning) && _deepEquals(socksPort, other.socksPort) && _deepEquals(bootstrapProgress, other.bootstrapProgress) && _deepEquals(currentCircuit, other.currentCircuit) && _deepEquals(exitNodeCountry, other.exitNodeCountry); + return _deepEquals(isRunning, other.isRunning) && + _deepEquals(socksPort, other.socksPort) && + _deepEquals(bootstrapProgress, other.bootstrapProgress) && + _deepEquals(currentCircuit, other.currentCircuit) && + _deepEquals(exitNodeCountry, other.exitNodeCountry); } @override @@ -277,15 +298,12 @@ class TorLogMessage { int timestamp; List _toList() { - return [ - severity, - message, - timestamp, - ]; + return [severity, message, timestamp]; } Object encode() { - return _toList(); } + return _toList(); + } static TorLogMessage decode(Object result) { result as List; @@ -305,7 +323,9 @@ class TorLogMessage { if (identical(this, other)) { return true; } - return _deepEquals(severity, other.severity) && _deepEquals(message, other.message) && _deepEquals(timestamp, other.timestamp); + return _deepEquals(severity, other.severity) && + _deepEquals(message, other.message) && + _deepEquals(timestamp, other.timestamp); } @override @@ -313,7 +333,6 @@ class TorLogMessage { int get hashCode => _deepHash([runtimeType, ..._toList()]); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -321,16 +340,16 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is TransportType) { + } else if (value is TransportType) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is TorConfiguration) { + } else if (value is TorConfiguration) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is TorStatus) { + } else if (value is TorStatus) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is TorLogMessage) { + } else if (value is TorLogMessage) { buffer.putUint8(132); writeValue(buffer, value.encode()); } else { @@ -362,8 +381,10 @@ class TorApi { /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. TorApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -373,27 +394,30 @@ class TorApi { /// Start Tor with the given configuration /// Returns a Future to avoid blocking the main thread Future startTor(TorConfiguration config) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.startTor$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_tor.TorApi.startTor$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([config]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [config], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as int; } /// Stop Tor Future stopTor() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.stopTor$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_tor.TorApi.stopTor$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -403,16 +427,16 @@ class TorApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Get current status Future getStatus() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.getStatus$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_tor.TorApi.getStatus$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -422,17 +446,17 @@ class TorApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as TorStatus; } /// Request a new Tor identity (new circuit) Future requestNewIdentity() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.requestNewIdentity$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_tor.TorApi.requestNewIdentity$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -442,11 +466,10 @@ class TorApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } @@ -460,12 +483,20 @@ abstract class TorLogApi { /// Called when status changes void onStatusChanged(TorStatus status); - static void setUp(TorLogApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + TorLogApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -477,16 +508,20 @@ abstract class TorLogApi { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } } { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -498,8 +533,10 @@ abstract class TorLogApi { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -511,9 +548,13 @@ class IPtProxyController { /// Constructor for [IPtProxyController]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - IPtProxyController({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + IPtProxyController({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -521,39 +562,43 @@ class IPtProxyController { final String pigeonVar_messageChannelSuffix; Future start(TransportType proxyType, String proxy) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.IPtProxyController.start$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_tor.IPtProxyController.start$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([proxyType, proxy]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [proxyType, proxy], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as int; } Future stop(TransportType proxyType) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.IPtProxyController.stop$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_tor.IPtProxyController.stop$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([proxyType]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [proxyType], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } diff --git a/packages/locale_resolver/lib/src/pigeons/locales.g.dart b/packages/locale_resolver/lib/src/pigeons/locales.g.dart index 32028b6d..52878eb3 100644 --- a/packages/locale_resolver/lib/src/pigeons/locales.g.dart +++ b/packages/locale_resolver/lib/src/pigeons/locales.g.dart @@ -10,9 +10,9 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; Object? _extractReplyValueOrThrow( - List? replyList, - String channelName, { - required bool isNullValid, + List? replyList, + String channelName, { + required bool isNullValid, }) { if (replyList == null) { throw PlatformException( @@ -46,8 +46,9 @@ bool _deepEquals(Object? a, Object? b) { } if (a is List && b is List) { return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); } if (a is Map && b is Map) { if (a.length != b.length) { @@ -96,26 +97,20 @@ int _deepHash(Object? value) { return value.hashCode; } - class LocalizedResult { - LocalizedResult({ - required this.languageName, - this.countryName, - }); + LocalizedResult({required this.languageName, this.countryName}); String languageName; String? countryName; List _toList() { - return [ - languageName, - countryName, - ]; + return [languageName, countryName]; } Object encode() { - return _toList(); } + return _toList(); + } static LocalizedResult decode(Object result) { result as List; @@ -134,7 +129,8 @@ class LocalizedResult { if (identical(this, other)) { return true; } - return _deepEquals(languageName, other.languageName) && _deepEquals(countryName, other.countryName); + return _deepEquals(languageName, other.languageName) && + _deepEquals(countryName, other.countryName); } @override @@ -142,7 +138,6 @@ class LocalizedResult { int get hashCode => _deepHash([runtimeType, ..._toList()]); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -150,7 +145,7 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is LocalizedResult) { + } else if (value is LocalizedResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); } else { @@ -173,31 +168,40 @@ class LocaleResolver { /// Constructor for [LocaleResolver]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - LocaleResolver({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + LocaleResolver({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; - Future resolve(String languageTag, String targetLangouageTag) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix'; + Future resolve( + String languageTag, + String targetLangouageTag, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([languageTag, targetLangouageTag]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [languageTag, targetLangouageTag], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as LocalizedResult; } } diff --git a/packages/simple_intent_receiver/lib/src/pigeons/intent.g.dart b/packages/simple_intent_receiver/lib/src/pigeons/intent.g.dart index 1a47ac74..ebb519ed 100644 --- a/packages/simple_intent_receiver/lib/src/pigeons/intent.g.dart +++ b/packages/simple_intent_receiver/lib/src/pigeons/intent.g.dart @@ -10,9 +10,9 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; Object? _extractReplyValueOrThrow( - List? replyList, - String channelName, { - required bool isNullValid, + List? replyList, + String channelName, { + required bool isNullValid, }) { if (replyList == null) { throw PlatformException( @@ -34,8 +34,11 @@ Object? _extractReplyValueOrThrow( return replyList.firstOrNull; } - -List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { +List wrapResponse({ + Object? result, + PlatformException? error, + bool empty = false, +}) { if (empty) { return []; } @@ -44,6 +47,7 @@ List wrapResponse({Object? result, PlatformException? error, bool empty } return [error.code, error.message, error.details]; } + bool _deepEquals(Object? a, Object? b) { if (identical(a, b)) { return true; @@ -56,8 +60,9 @@ bool _deepEquals(Object? a, Object? b) { } if (a is List && b is List) { return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); } if (a is Map && b is Map) { if (a.length != b.length) { @@ -106,7 +111,6 @@ int _deepHash(Object? value) { return value.hashCode; } - class Intent { Intent({ this.fromPackageName, @@ -141,7 +145,8 @@ class Intent { } Object encode() { - return _toList(); } + return _toList(); + } static Intent decode(Object result) { result as List; @@ -164,7 +169,12 @@ class Intent { if (identical(this, other)) { return true; } - return _deepEquals(fromPackageName, other.fromPackageName) && _deepEquals(action, other.action) && _deepEquals(data, other.data) && _deepEquals(categories, other.categories) && _deepEquals(mimeType, other.mimeType) && _deepEquals(extra, other.extra); + return _deepEquals(fromPackageName, other.fromPackageName) && + _deepEquals(action, other.action) && + _deepEquals(data, other.data) && + _deepEquals(categories, other.categories) && + _deepEquals(mimeType, other.mimeType) && + _deepEquals(extra, other.extra); } @override @@ -172,7 +182,6 @@ class Intent { int get hashCode => _deepHash([runtimeType, ..._toList()]); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -180,7 +189,7 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is Intent) { + } else if (value is Intent) { buffer.putUint8(129); writeValue(buffer, value.encode()); } else { @@ -203,9 +212,13 @@ class IntentHost { /// Constructor for [IntentHost]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - IntentHost({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + IntentHost({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -217,7 +230,8 @@ class IntentHost { /// IntentEvents.setUp() was called (cold-start deep links). /// Returns null if no launch intent is pending. Future getInitialIntent() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentHost.getInitialIntent$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.simple_intent_receiver.IntentHost.getInitialIntent$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -227,11 +241,10 @@ class IntentHost { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as Intent?; } } @@ -241,12 +254,20 @@ abstract class IntentEvents { void onIntentReceived(int sequence, Intent intent); - static void setUp(IntentEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + IntentEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -259,8 +280,10 @@ abstract class IntentEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } @@ -272,9 +295,13 @@ class IntentGatekeeperHostApi { /// Constructor for [IntentGatekeeperHostApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - IntentGatekeeperHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + IntentGatekeeperHostApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -284,42 +311,46 @@ class IntentGatekeeperHostApi { /// Replicates the blocked-packages policy to the native side so the /// [IntentReceiverActivity] can reject intents without launching Flutter. Future setConfig(bool enabled, List blockedPackages) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.setConfig$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.setConfig$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([enabled, blockedPackages]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [enabled, blockedPackages], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } /// Resolves a package name to its user-visible application label via /// [PackageManager]. Returns `null` if the package is not installed or the /// label cannot be resolved. Future resolvePackageLabel(String packageName) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.resolvePackageLabel$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.resolvePackageLabel$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([packageName]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [packageName], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); return pigeonVar_replyValue as String?; } @@ -329,7 +360,8 @@ class IntentGatekeeperHostApi { /// [ackPendingAlwaysAllows] after Flutter settings were updated /// successfully. Future> getPendingAlwaysAllows() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.getPendingAlwaysAllows$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.getPendingAlwaysAllows$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -339,31 +371,32 @@ class IntentGatekeeperHostApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } /// Removes the given packages from the pending "Always allow" set after /// Flutter has successfully persisted them into its own policy store. Future ackPendingAlwaysAllows(List packageNames) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.ackPendingAlwaysAllows$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.ackPendingAlwaysAllows$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([packageNames]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [packageNames], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); } } diff --git a/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart b/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart index afe8a3e3..a7d6ac11 100644 --- a/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart +++ b/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart @@ -10,9 +10,9 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; Object? _extractReplyValueOrThrow( - List? replyList, - String channelName, { - required bool isNullValid, + List? replyList, + String channelName, { + required bool isNullValid, }) { if (replyList == null) { throw PlatformException( @@ -34,8 +34,11 @@ Object? _extractReplyValueOrThrow( return replyList.firstOrNull; } - -List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { +List wrapResponse({ + Object? result, + PlatformException? error, + bool empty = false, +}) { if (empty) { return []; } @@ -45,7 +48,6 @@ List wrapResponse({Object? result, PlatformException? error, bool empty return [error.code, error.message, error.details]; } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -72,9 +74,13 @@ class SpeechToTextApi { /// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + SpeechToTextApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -89,21 +95,23 @@ class SpeechToTextApi { /// The [locale] parameter specifies the language locale for recognition /// (e.g., 'en-US', 'de-DE'). If null, uses the device default. Future showDialog({String? locale}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([locale]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [locale], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as bool; } } @@ -118,12 +126,20 @@ abstract class SpeechToTextEvents { /// recognition failed or was cancelled. void onTextReceived(String text); - static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + SpeechToTextEvents? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; { final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { @@ -135,8 +151,10 @@ abstract class SpeechToTextEvents { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); } }); } diff --git a/pubspec.yaml b/pubspec.yaml index cbd2c091..68ba8d44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,8 @@ workspace: - packages/locale_resolver/example - packages/speech_to_text_dialog - packages/speech_to_text_dialog/example + - packages/flutter_singbox_proxy + - packages/flutter_singbox_proxy/example dev_dependencies: melos: ^7.7.0 @@ -77,6 +79,9 @@ melos: sh scripts/build-container-proxy.sh sh scripts/build-readability.sh python3 scripts/build_quotes_db.py + build-singbox-libbox: + description: Build official sing-box Android libbox AAR for flutter_singbox_proxy + run: packages/flutter_singbox_proxy/scripts/build-libbox-android.sh build-browser: description: Build stable release APK split per ABI run: |