diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f5dc17e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build + +on: + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Cache Gradle dependencies + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build plugin + run: ./gradlew build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d879607 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Release +run-name: Release ${{ inputs.version }} + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 1.0.1)' + required: true + type: string + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Cache Gradle dependencies + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build plugin + run: ./gradlew build + + - name: Package addon zip + run: | + cd demo + zip -r "../GodotFirebaseAndroid-${{ inputs.version }}.zip" addons/GodotFirebaseAndroid + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ inputs.version }}" \ + --title "GodotFirebaseAndroid ${{ inputs.version }}" \ + --generate-notes \ + "GodotFirebaseAndroid-${{ inputs.version }}.zip" diff --git a/.gitignore b/.gitignore index 347e252..2489dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ google-services.json # Android Profiling *.hprof + +# Claude Code +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dc380b1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +GodotFirebaseAndroid is an Android plugin for Godot Engine (4.2+) that bridges GDScript and the native Android Firebase SDK. It follows a dual-layer architecture: Kotlin classes handle Firebase SDK interaction and async operations on the Android side, while thin GDScript wrappers in `firebase/export_scripts_template/` provide the developer-facing API via signals. + +**Communication flow:** GDScript → Module .gd wrapper → Kotlin plugin singleton → Firebase SDK → Firebase Backend. All long-running operations use Godot signals (not blocking calls). + +## Build Commands + +```bash +./gradlew assemble # Build debug+release AARs, copy to demo/addons/ +./gradlew build # Build AARs without copying +./gradlew clean # Clean build outputs and demo addons +``` + +The build produces `firebase-debug.aar` and `firebase-release.aar`, copies them to `demo/addons/GodotFirebaseAndroid/bin/`, and copies `export_scripts_template/` into the demo addon directory. + +There are no tests or linters configured. + +## Architecture + +### Kotlin layer (`firebase/src/main/java/org/godotengine/plugin/firebase/`) + +- **FirebasePlugin.kt** — Main Godot plugin class. Registers all signals and delegates calls to module classes. All module classes are instantiated here and receive the plugin reference. +- **Authentication.kt** — Anonymous, email/password, Google Sign-In, email verification, password reset, account linking. +- **Firestore.kt** — CRUD operations, collection queries, real-time listeners. +- **RealtimeDatabase.kt** — Path-based CRUD, real-time listeners. +- **CloudStorage.kt** — Upload/download, metadata, file listing. +- **Analytics.kt** — Event logging, user properties/ID. +- **RemoteConfig.kt** — Fetch, activate, typed getters. + +Each Kotlin module emits signals back to Godot via `emitSignal()` for async results. + +### GDScript layer (`firebase/export_scripts_template/`) + +- **Firebase.gd** — Autoloaded singleton that initializes module wrappers. +- **modules/*.gd** — One wrapper per Firebase module. Each connects to the Kotlin plugin singleton's signals and exposes snake_case methods. +- **export_plugin.gd** — Godot export plugin that handles adding dependencies and `google-services.json` during Android export. +- **plugin.cfg** — Godot plugin descriptor. + +### Demo project (`demo/`) + +Contains a Godot project with one scene per module for manual testing. The demo's `addons/` directory is auto-populated by the build. + +## Key Details + +- **Godot version:** 4.6+ (uses Godot Android plugin v2 API) +- **Android:** minSdk 24, targetSdk 34 +- **Kotlin DSL** for all Gradle files +- **No `google-services.json` in repo** — must be supplied per-project in `demo/android/build/` +- **Fork:** origin is SomniGameStudios, upstream is syntaxerror247/GodotFirebaseAndroid +- **PRs:** Always open pull requests against the fork repo (`SomniGameStudios/godot-firebase-android`), never against upstream + +## Adding a New Firebase Module + +1. Create Kotlin class in `firebase/src/main/java/.../firebase/` implementing the Firebase SDK calls with `emitSignal()` callbacks +2. Register signals and expose methods in `FirebasePlugin.kt` +3. Create GDScript wrapper in `firebase/export_scripts_template/modules/` +4. Initialize the module in `Firebase.gd` +5. Add Firebase dependency in `firebase/build.gradle.kts` +6. Add demo scene in `demo/scenes/` diff --git a/README.md b/README.md index df4cde0..3d8c6bd 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,12 @@ It supports Godot 4.2+ ## Features -- [x] Firebase Authentication (Anonymous, Email/Password, Google Sign-In) -- [x] Cloud Firestore +- [x] Firebase Authentication (Anonymous, Email/Password, Google Sign-In, reauthentication, profile management, auth state listener) +- [x] Cloud Firestore (CRUD, queries, real-time listeners, WriteBatch, transactions, FieldValue helpers) - [x] Realtime Database - [x] Cloud Storage +- [x] Firebase Analytics (event logging, user properties, consent management, session timeout) +- [x] Remote Config (fetch/activate, typed getters, real-time updates, value source tracking) - [ ] Cloud Messaging (coming soon) --- diff --git a/demo/export_presets.cfg b/demo/export_presets.cfg index f4bfeea..66edd38 100644 --- a/demo/export_presets.cfg +++ b/demo/export_presets.cfg @@ -3,7 +3,6 @@ name="Android" platform="Android" runnable=true -advanced_options=true dedicated_server=false custom_features="" export_filter="all_resources" @@ -11,6 +10,11 @@ include_filter="" exclude_filter="" export_path="../../../Desktop/GodotFirebaseAndroid.apk" patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" encryption_include_filters="" encryption_exclude_filters="" seed=0 @@ -29,6 +33,7 @@ gradle_build/compress_native_libraries=false gradle_build/export_format=0 gradle_build/min_sdk="" gradle_build/target_sdk="" +gradle_build/custom_theme_attributes={} architectures/armeabi-v7a=false architectures/arm64-v8a=true architectures/x86=false @@ -49,13 +54,16 @@ launcher_icons/adaptive_foreground_432x432="" launcher_icons/adaptive_background_432x432="" launcher_icons/adaptive_monochrome_432x432="" graphics/opengl_debug=false +shader_baker/enabled=false xr_features/xr_mode=0 gesture/swipe_to_dismiss=false screen/immersive_mode=false +screen/edge_to_edge=false screen/support_small=true screen/support_normal=true screen/support_large=true screen/support_xlarge=true +screen/background_color=Color(0, 0, 0, 1) user_data_backup/allow=false command_line/extra_args="" apk_expansion/enable=false @@ -136,6 +144,7 @@ permissions/manage_accounts=false permissions/manage_app_tokens=false permissions/manage_documents=false permissions/manage_external_storage=false +permissions/manage_media=false permissions/master_clear=false permissions/media_content_control=false permissions/modify_audio_settings=false diff --git a/demo/main.gd b/demo/main.gd index 17b9bac..ed6747d 100644 --- a/demo/main.gd +++ b/demo/main.gd @@ -4,6 +4,8 @@ var auth = load("res://scenes/authentication.tscn") var firestore = load("res://scenes/firestore.tscn") var realtimeDB = load("res://scenes/realtime_db.tscn") var storage = load("res://scenes/storage.tscn") +var remote_config = load("res://scenes/remote_config.tscn") +var analytics = load("res://scenes/analytics.tscn") func _on_auth_pressed() -> void: get_tree().change_scene_to_packed(auth) @@ -19,3 +21,11 @@ func _on_realtime_db_pressed() -> void: func _on_storage_pressed() -> void: get_tree().change_scene_to_packed(storage) + + +func _on_remote_config_pressed() -> void: + get_tree().change_scene_to_packed(remote_config) + + +func _on_analytics_pressed() -> void: + get_tree().change_scene_to_packed(analytics) diff --git a/demo/main.tscn b/demo/main.tscn index a09432d..6eb77cb 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -1,34 +1,34 @@ -[gd_scene load_steps=5 format=3 uid="uid://dppun5r2ki736"] +[gd_scene format=3 uid="uid://dppun5r2ki736"] [ext_resource type="Script" uid="uid://b56wdetifixsv" path="res://main.gd" id="1_ig7tw"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0xm2m"] -draw_center = false -border_width_left = 3 -border_width_top = 3 -border_width_right = 3 -border_width_bottom = 3 -border_color = Color(0.14902, 0.117647, 0.854902, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h2yge"] +bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h2yge"] -bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1bvp3"] +bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1bvp3"] -bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0xm2m"] +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.14902, 0.117647, 0.854902, 1) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[node name="Main" type="Control"] +[node name="Main" type="Control" unique_id=1472590519] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -37,7 +37,7 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_ig7tw") -[node name="ColorRect" type="ColorRect" parent="."] +[node name="ColorRect" type="ColorRect" parent="." unique_id=2055041397] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -46,7 +46,7 @@ grow_horizontal = 2 grow_vertical = 2 color = Color(0.133333, 0.133333, 0.133333, 1) -[node name="MarginContainer" type="MarginContainer" parent="."] +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1454730869] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -58,52 +58,74 @@ theme_override_constants/margin_top = 100 theme_override_constants/margin_right = 60 theme_override_constants/margin_bottom = 30 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=1853671400] layout_mode = 2 theme_override_constants/separation = 40 alignment = 1 -[node name="Auth" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="Auth" type="Button" parent="MarginContainer/VBoxContainer" unique_id=510532281] custom_minimum_size = Vector2(120, 80) layout_mode = 2 theme_override_font_sizes/font_size = 36 -theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") -theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") -theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") theme_override_styles/normal = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") +theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") text = "Authentication" -[node name="Firestore" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="Firestore" type="Button" parent="MarginContainer/VBoxContainer" unique_id=1356560750] custom_minimum_size = Vector2(120, 80) layout_mode = 2 theme_override_font_sizes/font_size = 36 -theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") -theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") -theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") theme_override_styles/normal = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") +theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") text = "Firestore" -[node name="RealtimeDB" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="RealtimeDB" type="Button" parent="MarginContainer/VBoxContainer" unique_id=554033220] custom_minimum_size = Vector2(120, 80) layout_mode = 2 theme_override_font_sizes/font_size = 36 -theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") -theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") -theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") theme_override_styles/normal = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") +theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") text = "Realtime Database" -[node name="Storage" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="Storage" type="Button" parent="MarginContainer/VBoxContainer" unique_id=1974473671] custom_minimum_size = Vector2(120, 80) layout_mode = 2 theme_override_font_sizes/font_size = 36 -theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/normal = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") +text = "Storage" + +[node name="RemoteConfig" type="Button" parent="MarginContainer/VBoxContainer" unique_id=2050538536] +custom_minimum_size = Vector2(120, 80) +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +theme_override_styles/normal = SubResource("StyleBoxFlat_h2yge") theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") +theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") +text = "Remote Config" + +[node name="Analytics" type="Button" parent="MarginContainer/VBoxContainer" unique_id=1510952869] +custom_minimum_size = Vector2(120, 80) +layout_mode = 2 +theme_override_font_sizes/font_size = 36 theme_override_styles/normal = SubResource("StyleBoxFlat_h2yge") -text = "Storage" +theme_override_styles/pressed = SubResource("StyleBoxFlat_1bvp3") +theme_override_styles/hover = SubResource("StyleBoxFlat_h2yge") +theme_override_styles/focus = SubResource("StyleBoxFlat_0xm2m") +text = "Analytics" [connection signal="pressed" from="MarginContainer/VBoxContainer/Auth" to="." method="_on_auth_pressed"] [connection signal="pressed" from="MarginContainer/VBoxContainer/Firestore" to="." method="_on_firestore_pressed"] [connection signal="pressed" from="MarginContainer/VBoxContainer/RealtimeDB" to="." method="_on_realtime_db_pressed"] [connection signal="pressed" from="MarginContainer/VBoxContainer/Storage" to="." method="_on_storage_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/RemoteConfig" to="." method="_on_remote_config_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Analytics" to="." method="_on_analytics_pressed"] diff --git a/demo/project.godot b/demo/project.godot index 25b9e2d..c97b641 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -14,7 +14,7 @@ config/name="Godot Firebase Android" config/tags=PackedStringArray("android", "demo", "plugin") run/main_scene="res://main.tscn" config/quit_on_go_back=false -config/features=PackedStringArray("4.4", "GL Compatibility") +config/features=PackedStringArray("4.6", "GL Compatibility") run/low_processor_mode=true config/icon="res://icon.svg" diff --git a/demo/scenes/analytics.gd b/demo/scenes/analytics.gd new file mode 100644 index 0000000..f37daea --- /dev/null +++ b/demo/scenes/analytics.gd @@ -0,0 +1,104 @@ +extends Control + +@onready var output: RichTextLabel = %OutputPanel +var _collection_enabled := true + +func _notification(what: int) -> void: + if what == NOTIFICATION_WM_GO_BACK_REQUEST: + get_tree().change_scene_to_packed(load("res://main.tscn")) + +func _ready() -> void: + Firebase.analytics.app_instance_id_result.connect(_on_instance_id) + _log("platform", "Android") + +# --- Events --- + +func _on_log_event_pressed() -> void: + _log("action", "Logging custom event 'test_event'...") + Firebase.analytics.log_event("test_event", { + "item_name": "sword_of_fire", + "item_category": "weapons", + "value": 100 + }) + _log("done", "Event logged (check Firebase Console)") + +func _on_log_purchase_pressed() -> void: + _log("action", "Logging purchase event...") + Firebase.analytics.log_event("purchase", { + "currency": "USD", + "value": 9.99, + "item_id": "gem_pack_100" + }) + _log("done", "Purchase event logged") + +# --- User --- + +func _on_set_user_id_pressed() -> void: + _log("action", "Setting user ID to 'test_user_123'...") + Firebase.analytics.set_user_id("test_user_123") + _log("done", "User ID set") + +func _on_set_user_property_pressed() -> void: + _log("action", "Setting user property 'favorite_food' = 'pizza'...") + Firebase.analytics.set_user_property("favorite_food", "pizza") + _log("done", "User property set") + +func _on_set_default_params_pressed() -> void: + _log("action", "Setting default event parameters...") + Firebase.analytics.set_default_event_parameters({ + "app_version": "1.0.0", + "platform": "android" + }) + _log("done", "Default parameters set") + +# --- Diagnostics --- + +func _on_get_instance_id_pressed() -> void: + _log("action", "Requesting app instance ID...") + Firebase.analytics.get_app_instance_id() + +func _on_instance_id(id: String) -> void: + _log("instance_id", id if not id.is_empty() else "(empty)") + +func _on_reset_analytics_pressed() -> void: + _log("action", "Resetting analytics data...") + Firebase.analytics.reset_analytics_data() + _log("done", "Analytics data reset") + +func _on_toggle_collection_pressed() -> void: + _collection_enabled = not _collection_enabled + _log("action", "Setting analytics collection to %s..." % str(_collection_enabled)) + Firebase.analytics.set_analytics_collection_enabled(_collection_enabled) + _log("done", "Collection %s" % ("enabled" if _collection_enabled else "disabled")) + +# --- Consent & Session (Android-only) --- + +func _on_set_consent_granted_pressed() -> void: + _log("action", "Granting all consent...") + Firebase.analytics.set_consent(true, true, true, true) + _log("done", "All consent granted") + +func _on_set_consent_denied_pressed() -> void: + _log("action", "Denying all consent...") + Firebase.analytics.set_consent(false, false, false, false) + _log("done", "All consent denied") + +func _on_set_session_timeout_pressed() -> void: + _log("action", "Setting session timeout to 300s...") + Firebase.analytics.set_session_timeout(300) + _log("done", "Session timeout set to 300s") + +# --- Logging --- + +func _log(context: String, message: String) -> void: + var t = Time.get_time_string_from_system() + output.text += "[%s] %s: %s\n" % [t, context, message] + if not is_inside_tree(): + return + await get_tree().process_frame + if not is_inside_tree(): + return + output.scroll_to_line(output.get_line_count()) + +func _on_clear_output_pressed() -> void: + output.text = "" diff --git a/demo/scenes/analytics.gd.uid b/demo/scenes/analytics.gd.uid new file mode 100644 index 0000000..404f29e --- /dev/null +++ b/demo/scenes/analytics.gd.uid @@ -0,0 +1 @@ +uid://dv8c30ior4w8h diff --git a/demo/scenes/analytics.tscn b/demo/scenes/analytics.tscn new file mode 100644 index 0000000..14a1d05 --- /dev/null +++ b/demo/scenes/analytics.tscn @@ -0,0 +1,215 @@ +[gd_scene format=3 uid="uid://crpwtp2y06rh7"] + +[ext_resource type="Script" uid="uid://dv8c30ior4w8h" path="res://scenes/analytics.gd" id="1_analytics"] +[ext_resource type="Script" uid="uid://c1lwfy11opayr" path="res://scenes/scroll_container.gd" id="2_scroll"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_normal"] +bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pressed"] +bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_focus"] +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.14902, 0.117647, 0.854902, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[node name="Analytics" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_analytics") + +[node name="ColorRect" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.133333, 0.133333, 0.133333, 1) + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 40 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 40 +theme_override_constants/margin_bottom = 20 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 +script = ExtResource("2_scroll") + +[node name="ButtonContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 20 + +[node name="log_event" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Log Custom Event" + +[node name="log_purchase" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Log Purchase Event" + +[node name="set_user_id" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Set User ID" + +[node name="set_user_property" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Set User Property" + +[node name="set_default_params" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Set Default Params" + +[node name="get_instance_id" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Get Instance ID" + +[node name="reset_analytics" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Reset Analytics" + +[node name="toggle_collection" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Toggle Collection" + +[node name="set_consent_granted" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Grant All Consent" + +[node name="set_consent_denied" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Deny All Consent" + +[node name="set_session_timeout" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Set Session Timeout (300s)" + +[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 200) +layout_mode = 2 +theme_override_font_sizes/normal_font_size = 24 +scroll_following = true + +[node name="clear_output" type="Button" parent="MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Clear Output" + +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/log_event" to="." method="_on_log_event_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/log_purchase" to="." method="_on_log_purchase_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_user_id" to="." method="_on_set_user_id_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_user_property" to="." method="_on_set_user_property_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_default_params" to="." method="_on_set_default_params_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_instance_id" to="." method="_on_get_instance_id_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/reset_analytics" to="." method="_on_reset_analytics_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/toggle_collection" to="." method="_on_toggle_collection_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_consent_granted" to="." method="_on_set_consent_granted_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_consent_denied" to="." method="_on_set_consent_denied_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_session_timeout" to="." method="_on_set_session_timeout_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/clear_output" to="." method="_on_clear_output_pressed"] diff --git a/demo/scenes/authentication.gd b/demo/scenes/authentication.gd index 7190091..084cdeb 100644 --- a/demo/scenes/authentication.gd +++ b/demo/scenes/authentication.gd @@ -2,8 +2,9 @@ extends Control @onready var output_panel = $MarginContainer/VBoxContainer/OutputPanel -@onready var email = $MarginContainer/VBoxContainer/LineEdit -@onready var password = $MarginContainer/VBoxContainer/LineEdit2 +@onready var email = $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/LineEdit +@onready var password = $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/LineEdit2 +@onready var display_name_input = $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/display_name_input func _notification(what: int) -> void: if what == NOTIFICATION_WM_GO_BACK_REQUEST: @@ -19,9 +20,24 @@ func _ready() -> void: Firebase.auth.email_verification_sent.connect(print_output.bind("email_verification_sent")) Firebase.auth.password_reset_sent.connect(print_output.bind("password_reset_sent")) Firebase.auth.user_deleted.connect(print_output.bind("user_deleted")) + Firebase.auth.auth_state_changed.connect(_on_auth_state_changed) + Firebase.auth.id_token_result.connect(print_output.bind("id_token_result")) + Firebase.auth.id_token_error.connect(print_output.bind("id_token_error")) + Firebase.auth.profile_updated.connect(print_output.bind("profile_updated")) + Firebase.auth.profile_update_failure.connect(print_output.bind("profile_update_failure")) + + +func _log(message: String) -> void: + var time = Time.get_time_string_from_system() + output_panel.text += "[%s] %s\n" % [time, message] + func print_output(arg, context: String): - output_panel.text += context + ": " +str(arg) + "\n" + _log(context + ": " + str(arg)) + + +func _on_clear_output_pressed() -> void: + output_panel.text = "" func _on_anonymous_sign_in_pressed() -> void: @@ -49,7 +65,7 @@ func _on_get_user_data_pressed() -> void: func _on_is_signed_in_pressed() -> void: - print_output(Firebase.auth.is_signed_in(),"Is SignedIn") + print_output(Firebase.auth.is_signed_in(), "Is SignedIn") func _on_sign_out_pressed() -> void: @@ -66,3 +82,46 @@ func _on_email_verification_pressed() -> void: func _on_password_reset_pressed() -> void: Firebase.auth.send_password_reset_email(email.text) + + +func _on_auth_state_changed(signed_in: bool, current_user_data) -> void: + _log("auth_state_changed: signed_in=" + str(signed_in) + " data=" + str(current_user_data)) + + +func _on_reauthenticate_pressed() -> void: + Firebase.auth.reauthenticate_with_email(email.text, password.text) + + +func _on_add_auth_listener_pressed() -> void: + Firebase.auth.add_auth_state_listener() + _log("Auth state listener added") + + +func _on_remove_auth_listener_pressed() -> void: + Firebase.auth.remove_auth_state_listener() + _log("Auth state listener removed") + + +func _on_get_id_token_pressed() -> void: + Firebase.auth.get_id_token(false) + + +func _on_update_profile_pressed() -> void: + Firebase.auth.update_profile(display_name_input.text) + + +func _on_update_password_pressed() -> void: + Firebase.auth.update_password(password.text) + + +func _on_reload_user_pressed() -> void: + Firebase.auth.reload_user() + + +func _on_unlink_google_pressed() -> void: + Firebase.auth.unlink_provider("google.com") + + +func _on_use_emulator_pressed() -> void: + Firebase.auth.use_emulator("10.0.2.2", 9099) + _log("Auth emulator set to 10.0.2.2:9099") diff --git a/demo/scenes/authentication.tscn b/demo/scenes/authentication.tscn index fef911e..da61be1 100644 --- a/demo/scenes/authentication.tscn +++ b/demo/scenes/authentication.tscn @@ -1,34 +1,35 @@ -[gd_scene load_steps=5 format=3 uid="uid://b4wla4tiq5v36"] +[gd_scene format=3 uid="uid://b4wla4tiq5v36"] [ext_resource type="Script" uid="uid://cbqguua1lmw1q" path="res://scenes/authentication.gd" id="1_3b5bi"] +[ext_resource type="Script" uid="uid://c1lwfy11opayr" path="res://scenes/scroll_container.gd" id="2_scroll"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7dm0k"] -draw_center = false -border_width_left = 3 -border_width_top = 3 -border_width_right = 3 -border_width_bottom = 3 -border_color = Color(0.14902, 0.117647, 0.854902, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ig7tw"] +bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ig7tw"] -bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0xm2m"] +bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0xm2m"] -bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7dm0k"] +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.14902, 0.117647, 0.854902, 1) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[node name="Authentication" type="Control"] +[node name="Authentication" type="Control" unique_id=1516008471] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -37,7 +38,7 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_3b5bi") -[node name="ColorRect" type="ColorRect" parent="."] +[node name="ColorRect" type="ColorRect" parent="." unique_id=1301935428] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -46,7 +47,7 @@ grow_horizontal = 2 grow_vertical = 2 color = Color(0.133333, 0.133333, 0.133333, 1) -[node name="MarginContainer" type="MarginContainer" parent="."] +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1095550827] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -58,146 +59,273 @@ theme_override_constants/margin_top = 20 theme_override_constants/margin_right = 40 theme_override_constants/margin_bottom = 20 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=1580796319] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer" unique_id=45920927] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 +script = ExtResource("2_scroll") + +[node name="ButtonContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer" unique_id=1862006050] layout_mode = 2 -theme_override_constants/separation = 36 -alignment = 1 +size_flags_horizontal = 3 +theme_override_constants/separation = 20 -[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer"] +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1237815704] custom_minimum_size = Vector2(0, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 placeholder_text = "Email" -[node name="LineEdit2" type="LineEdit" parent="MarginContainer/VBoxContainer"] +[node name="LineEdit2" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1798937198] custom_minimum_size = Vector2(0, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 placeholder_text = "Password" -[node name="anonymous_sign_in" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="anonymous_sign_in" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=176216697] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Sign In Anonymously" -[node name="email_sign_up" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="email_sign_up" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=935803591] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Create User With Email Password" -[node name="email_sign_in" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="email_sign_in" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1559812975] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Sign In With Email Password" -[node name="email_verification" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="email_verification" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1671620727] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Send Email Verification" -[node name="password_reset" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="password_reset" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1302125307] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Send Password Reset Email" -[node name="google_sign_in" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="google_sign_in" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1871919082] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Sign In With Google" -[node name="link_anonymous_with_google" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="link_anonymous_with_google" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=481102813] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Link Anonymous To Google" -[node name="get_user_data" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="get_user_data" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=2078776940] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Get Current User Data" -[node name="is_signed_in" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="is_signed_in" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1880020358] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Is Signed In" + +[node name="sign_out" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=165805541] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Sign Out" + +[node name="delete_user" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=215148053] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Delete Current User" + +[node name="display_name_input" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(0, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +placeholder_text = "Display Name" + +[node name="reauthenticate" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") -text = "Is Signed In" +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Reauthenticate (Email)" -[node name="sign_out" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="add_auth_listener" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Add Auth State Listener" + +[node name="remove_auth_listener" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Remove Auth State Listener" + +[node name="get_id_token" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Get ID Token" + +[node name="update_profile" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") -text = "Sign Out" +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Update Profile" -[node name="delete_user" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="update_password" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Update Password" + +[node name="reload_user" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Reload User" + +[node name="unlink_google" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Unlink Google Provider" + +[node name="use_emulator" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") -text = "Delete Current User" +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Use Auth Emulator" -[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"] +[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer" unique_id=976434836] +custom_minimum_size = Vector2(0, 200) layout_mode = 2 -size_flags_vertical = 3 -theme_override_font_sizes/normal_font_size = 32 - -[connection signal="pressed" from="MarginContainer/VBoxContainer/anonymous_sign_in" to="." method="_on_anonymous_sign_in_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/email_sign_up" to="." method="_on_email_sign_up_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/email_sign_in" to="." method="_on_email_sign_in_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/email_verification" to="." method="_on_email_verification_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/password_reset" to="." method="_on_password_reset_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/google_sign_in" to="." method="_on_google_sign_in_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/link_anonymous_with_google" to="." method="_on_link_anonymous_with_google_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/get_user_data" to="." method="_on_get_user_data_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/is_signed_in" to="." method="_on_is_signed_in_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/sign_out" to="." method="_on_sign_out_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/delete_user" to="." method="_on_delete_user_pressed"] +theme_override_font_sizes/normal_font_size = 24 +scroll_following = true + +[node name="clear_output" type="Button" parent="MarginContainer/VBoxContainer" unique_id=869574200] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Clear Output" + +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/anonymous_sign_in" to="." method="_on_anonymous_sign_in_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/email_sign_up" to="." method="_on_email_sign_up_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/email_sign_in" to="." method="_on_email_sign_in_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/email_verification" to="." method="_on_email_verification_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/password_reset" to="." method="_on_password_reset_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/google_sign_in" to="." method="_on_google_sign_in_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/link_anonymous_with_google" to="." method="_on_link_anonymous_with_google_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_user_data" to="." method="_on_get_user_data_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/is_signed_in" to="." method="_on_is_signed_in_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/sign_out" to="." method="_on_sign_out_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/delete_user" to="." method="_on_delete_user_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/reauthenticate" to="." method="_on_reauthenticate_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/add_auth_listener" to="." method="_on_add_auth_listener_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/remove_auth_listener" to="." method="_on_remove_auth_listener_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_id_token" to="." method="_on_get_id_token_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/update_profile" to="." method="_on_update_profile_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/update_password" to="." method="_on_update_password_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/reload_user" to="." method="_on_reload_user_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/unlink_google" to="." method="_on_unlink_google_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/use_emulator" to="." method="_on_use_emulator_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/clear_output" to="." method="_on_clear_output_pressed"] diff --git a/demo/scenes/firestore.gd b/demo/scenes/firestore.gd index bcff186..35466f8 100644 --- a/demo/scenes/firestore.gd +++ b/demo/scenes/firestore.gd @@ -1,7 +1,8 @@ extends Control -@onready var collection = $MarginContainer/VBoxContainer/HBoxContainer/collection -@onready var docID = $MarginContainer/VBoxContainer/HBoxContainer/docID +@onready var output_panel = $MarginContainer/VBoxContainer/OutputPanel +@onready var collection = $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer/collection +@onready var docID = $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer/docID @onready var pair_container = $ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container func _notification(what: int) -> void: @@ -15,6 +16,15 @@ func _ready() -> void: Firebase.firestore.update_task_completed.connect(print_output.bind("update_task_completed")) Firebase.firestore.delete_task_completed.connect(print_output.bind("delete_task_completed")) Firebase.firestore.document_changed.connect(print_listner_output.bind("document_changed")) + Firebase.firestore.query_task_completed.connect(print_output.bind("query_task_completed")) + Firebase.firestore.collection_changed.connect(print_listner_output.bind("collection_changed")) + Firebase.firestore.batch_task_completed.connect(print_output.bind("batch_task_completed")) + Firebase.firestore.transaction_task_completed.connect(print_output.bind("transaction_task_completed")) + + +func _log(message: String) -> void: + var time = Time.get_time_string_from_system() + output_panel.text += "[%s] %s\n" % [time, message] func get_dictionary_from_inputs() -> Dictionary: @@ -29,11 +39,17 @@ func get_dictionary_from_inputs() -> Dictionary: data_dict[key] = value return data_dict + func print_output(arg, context: String): - $MarginContainer/VBoxContainer/OutputPanel.text += context + ": " +str(arg) + "\n" + _log(context + ": " + str(arg)) + func print_listner_output(arg, arg2, context: String): - $MarginContainer/VBoxContainer/OutputPanel.text += context + ": " +str(arg) + " -|- " +str(arg2) + "\n" + _log(context + ": " + str(arg) + " -|- " + str(arg2)) + + +func _on_clear_output_pressed() -> void: + output_panel.text = "" func _on_add_document_pressed() -> void: @@ -41,7 +57,7 @@ func _on_add_document_pressed() -> void: func _on_set_document_pressed() -> void: - Firebase.firestore.set_document(collection.text, docID.text, get_dictionary_from_inputs(), $MarginContainer/VBoxContainer/HBoxContainer2/merge.button_pressed) + Firebase.firestore.set_document(collection.text, docID.text, get_dictionary_from_inputs(), $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer2/merge.button_pressed) func _on_get_document_pressed() -> void: @@ -61,12 +77,56 @@ func _on_delete_document_pressed() -> void: func _on_listen_to_document_pressed() -> void: - Firebase.firestore.listen_to_document(collection.text+"/"+docID.text) + Firebase.firestore.listen_to_document(collection.text + "/" + docID.text) func _on_stop_listening_to_document_pressed() -> void: - Firebase.firestore.stop_listening_to_document(collection.text+"/"+docID.text) + Firebase.firestore.stop_listening_to_document(collection.text + "/" + docID.text) + + +func _on_query_documents_pressed() -> void: + var filters = [{"field": "score", "op": ">=", "value": 100}] + _log("query: collection=%s filters=%s" % [collection.text, str(filters)]) + Firebase.firestore.query_documents(collection.text, filters, "", false, 10) + + +func _on_listen_to_collection_pressed() -> void: + _log("Listening to collection: %s" % collection.text) + Firebase.firestore.listen_to_collection(collection.text) + + +func _on_stop_listening_collection_pressed() -> void: + _log("Stopped listening to collection: %s" % collection.text) + Firebase.firestore.stop_listening_to_collection(collection.text) + + +func _on_run_batch_demo_pressed() -> void: + var batch_id = Firebase.firestore.create_batch() + _log("Created batch %s" % str(batch_id)) + Firebase.firestore.batch_set(batch_id, collection.text, "batch_doc_1", {"name": "Batch Set", "value": 1}, false) + Firebase.firestore.batch_update(batch_id, collection.text, docID.text, {"batch_updated": true}) + Firebase.firestore.commit_batch(batch_id) + _log("Commit requested for batch %s" % str(batch_id)) + + +func _on_run_transaction_pressed() -> void: + _log("Running transaction on %s/%s" % [collection.text, docID.text]) + Firebase.firestore.run_transaction(collection.text, docID.text, get_dictionary_from_inputs()) + + +func _on_update_timestamp_pressed() -> void: + _log("Updating %s/%s with server timestamp" % [collection.text, docID.text]) + Firebase.firestore.update_document(collection.text, docID.text, {"updated_at": Firebase.firestore.server_timestamp()}) + + +func _on_increment_field_pressed() -> void: + _log("Incrementing 'count' by 1 on %s/%s" % [collection.text, docID.text]) + Firebase.firestore.update_document(collection.text, docID.text, {"count": Firebase.firestore.increment_by(1)}) + +func _on_use_emulator_pressed() -> void: + Firebase.firestore.use_emulator("10.0.2.2", 8080) + _log("Firestore emulator set to 10.0.2.2:8080") func _on_manage_data_pressed() -> void: @@ -77,7 +137,6 @@ func _on_add_pair_pressed() -> void: var new_pair = $ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair.duplicate() new_pair.show() pair_container.add_child(new_pair) - pass func _on_close_pressed() -> void: diff --git a/demo/scenes/firestore.tscn b/demo/scenes/firestore.tscn index f064a55..13bca97 100644 --- a/demo/scenes/firestore.tscn +++ b/demo/scenes/firestore.tscn @@ -1,35 +1,36 @@ -[gd_scene load_steps=8 format=3 uid="uid://by3lq5rrca21w"] +[gd_scene format=3 uid="uid://by3lq5rrca21w"] [ext_resource type="Script" uid="uid://b6ce0u3c1yf13" path="res://scenes/firestore.gd" id="1_m3na1"] +[ext_resource type="Script" uid="uid://c1lwfy11opayr" path="res://scenes/scroll_container.gd" id="2_scroll"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7dm0k"] -draw_center = false -border_width_left = 3 -border_width_top = 3 -border_width_right = 3 -border_width_bottom = 3 -border_color = Color(0.14902, 0.117647, 0.854902, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e3ux7"] +bg_color = Color(0.205117, 0.205117, 0.205117, 0.666667) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u4mew"] -bg_color = Color(0.205117, 0.205117, 0.205117, 0.666667) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0xm2m"] +bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0xm2m"] -bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u4mew"] +bg_color = Color(0.205117, 0.205117, 0.205117, 0.666667) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e3ux7"] -bg_color = Color(0.205117, 0.205117, 0.205117, 0.666667) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7dm0k"] +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.14902, 0.117647, 0.854902, 1) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 @@ -53,7 +54,7 @@ corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[node name="Firestore" type="Control"] +[node name="Firestore" type="Control" unique_id=922289781] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -62,7 +63,7 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_m3na1") -[node name="ColorRect" type="ColorRect" parent="."] +[node name="ColorRect" type="ColorRect" parent="." unique_id=259674543] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -71,7 +72,7 @@ grow_horizontal = 2 grow_vertical = 2 color = Color(0.133333, 0.133333, 0.133333, 1) -[node name="MarginContainer" type="MarginContainer" parent="."] +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=871231618] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -83,148 +84,249 @@ theme_override_constants/margin_top = 20 theme_override_constants/margin_right = 40 theme_override_constants/margin_bottom = 10 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=193312979] layout_mode = 2 -theme_override_constants/separation = 36 -alignment = 1 +theme_override_constants/separation = 10 -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer" unique_id=949936770] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 +script = ExtResource("2_scroll") + +[node name="ButtonContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer" unique_id=1145112974] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 20 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=24552472] custom_minimum_size = Vector2(0, 60) layout_mode = 2 theme_override_constants/separation = 30 -[node name="collection" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"] +[node name="collection" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer" unique_id=75553581] layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 placeholder_text = "collection" alignment = 1 -[node name="docID" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"] +[node name="docID" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer" unique_id=2036264701] layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 placeholder_text = "doc ID" alignment = 1 -[node name="manage_data" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="manage_data" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=586411721] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_u4mew") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_e3ux7") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_u4mew") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Manage Data" -[node name="add_document" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="add_document" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=2051241255] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Add Document" -[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1855434317] layout_mode = 2 theme_override_constants/separation = 20 -[node name="set_document" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer2"] +[node name="set_document" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer2" unique_id=78742402] custom_minimum_size = Vector2(120, 70) layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Set Document" -[node name="merge" type="CheckButton" parent="MarginContainer/VBoxContainer/HBoxContainer2"] +[node name="merge" type="CheckButton" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer2" unique_id=355159598] custom_minimum_size = Vector2(180, 0) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Merge" alignment = 1 -[node name="get_document" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="get_document" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1809301633] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Get Document" -[node name="get_documents_in_collection" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="get_documents_in_collection" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=171777807] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Get Documents in Collection" -[node name="HBoxContainer3" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="HBoxContainer3" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=105444189] layout_mode = 2 theme_override_constants/separation = 26 -[node name="update_document" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer3"] +[node name="update_document" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer3" unique_id=1626918856] custom_minimum_size = Vector2(120, 70) layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Update Document" -[node name="delete_document" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer3"] +[node name="delete_document" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer3" unique_id=81547966] custom_minimum_size = Vector2(120, 70) layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Delete Document" + +[node name="listen_to_document" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1359562151] +custom_minimum_size = Vector2(120, 70) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Listen To Document" + +[node name="stop_listening_to_document" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=928552225] +custom_minimum_size = Vector2(120, 70) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") -text = "Delete Document" +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Stop Listening To Document" -[node name="listen_to_document" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="query_documents" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Query Documents" + +[node name="listen_to_collection" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 70) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Listen To Collection" + +[node name="stop_listening_collection" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 70) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Stop Listening Collection" + +[node name="run_batch_demo" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 70) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") -text = "Listen To Document" +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Run Batch Demo" -[node name="stop_listening_to_document" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="run_transaction" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Run Transaction" + +[node name="update_timestamp" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 70) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Update With Timestamp" + +[node name="increment_field" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 70) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Increment 'count' By 1" + +[node name="use_emulator" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 70) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") -text = "Stop Listening To Document" +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Use Firestore Emulator" -[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"] +[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer" unique_id=1857031097] +custom_minimum_size = Vector2(0, 200) layout_mode = 2 -size_flags_vertical = 3 -theme_override_font_sizes/normal_font_size = 32 +theme_override_font_sizes/normal_font_size = 24 +scroll_following = true + +[node name="clear_output" type="Button" parent="MarginContainer/VBoxContainer" unique_id=602484466] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Clear Output" -[node name="ManageDataPanel" type="PanelContainer" parent="."] +[node name="ManageDataPanel" type="PanelContainer" parent="." unique_id=103502874] visible = false layout_mode = 1 anchors_preset = 8 @@ -240,24 +342,24 @@ grow_horizontal = 2 grow_vertical = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_pqa68") -[node name="VBoxContainer" type="VBoxContainer" parent="ManageDataPanel"] +[node name="VBoxContainer" type="VBoxContainer" parent="ManageDataPanel" unique_id=1697090302] layout_mode = 2 theme_override_constants/separation = 50 -[node name="ScrollContainer" type="ScrollContainer" parent="ManageDataPanel/VBoxContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="ManageDataPanel/VBoxContainer" unique_id=486227177] layout_mode = 2 size_flags_vertical = 3 -[node name="key_value_pair_container" type="VBoxContainer" parent="ManageDataPanel/VBoxContainer/ScrollContainer"] +[node name="key_value_pair_container" type="VBoxContainer" parent="ManageDataPanel/VBoxContainer/ScrollContainer" unique_id=389366767] layout_mode = 2 size_flags_horizontal = 3 -[node name="sample_pair" type="HBoxContainer" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container"] +[node name="sample_pair" type="HBoxContainer" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container" unique_id=273365] visible = false layout_mode = 2 theme_override_constants/separation = 30 -[node name="key" type="LineEdit" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair"] +[node name="key" type="LineEdit" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair" unique_id=1924403834] custom_minimum_size = Vector2(0, 60) layout_mode = 2 size_flags_horizontal = 3 @@ -265,7 +367,7 @@ theme_override_font_sizes/font_size = 32 placeholder_text = "key" alignment = 1 -[node name="value" type="LineEdit" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair"] +[node name="value" type="LineEdit" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair" unique_id=815229829] custom_minimum_size = Vector2(0, 60) layout_mode = 2 size_flags_horizontal = 3 @@ -273,46 +375,55 @@ theme_override_font_sizes/font_size = 32 placeholder_text = "values" alignment = 1 -[node name="CheckBox" type="CheckButton" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair"] +[node name="CheckBox" type="CheckButton" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair" unique_id=920107984] layout_mode = 2 theme_override_font_sizes/font_size = 32 text = "Numeric" -[node name="HBoxContainer" type="HBoxContainer" parent="ManageDataPanel/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="ManageDataPanel/VBoxContainer" unique_id=618761152] layout_mode = 2 theme_override_constants/separation = 30 alignment = 1 -[node name="close" type="Button" parent="ManageDataPanel/VBoxContainer/HBoxContainer"] +[node name="close" type="Button" parent="ManageDataPanel/VBoxContainer/HBoxContainer" unique_id=220915893] custom_minimum_size = Vector2(120, 70) layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Close" -[node name="add_pair" type="Button" parent="ManageDataPanel/VBoxContainer/HBoxContainer"] +[node name="add_pair" type="Button" parent="ManageDataPanel/VBoxContainer/HBoxContainer" unique_id=825826955] custom_minimum_size = Vector2(120, 70) layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "New Pair" -[connection signal="pressed" from="MarginContainer/VBoxContainer/manage_data" to="." method="_on_manage_data_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/add_document" to="." method="_on_add_document_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer2/set_document" to="." method="_on_set_document_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/get_document" to="." method="_on_get_document_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/get_documents_in_collection" to="." method="_on_get_documents_in_collection_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer3/update_document" to="." method="_on_update_document_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer3/delete_document" to="." method="_on_delete_document_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/listen_to_document" to="." method="_on_listen_to_document_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/stop_listening_to_document" to="." method="_on_stop_listening_to_document_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/manage_data" to="." method="_on_manage_data_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/add_document" to="." method="_on_add_document_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer2/set_document" to="." method="_on_set_document_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_document" to="." method="_on_get_document_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_documents_in_collection" to="." method="_on_get_documents_in_collection_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer3/update_document" to="." method="_on_update_document_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer3/delete_document" to="." method="_on_delete_document_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/listen_to_document" to="." method="_on_listen_to_document_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/stop_listening_to_document" to="." method="_on_stop_listening_to_document_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/query_documents" to="." method="_on_query_documents_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/listen_to_collection" to="." method="_on_listen_to_collection_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/stop_listening_collection" to="." method="_on_stop_listening_collection_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/run_batch_demo" to="." method="_on_run_batch_demo_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/run_transaction" to="." method="_on_run_transaction_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/update_timestamp" to="." method="_on_update_timestamp_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/increment_field" to="." method="_on_increment_field_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/use_emulator" to="." method="_on_use_emulator_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/clear_output" to="." method="_on_clear_output_pressed"] [connection signal="pressed" from="ManageDataPanel/VBoxContainer/HBoxContainer/close" to="." method="_on_close_pressed"] [connection signal="pressed" from="ManageDataPanel/VBoxContainer/HBoxContainer/add_pair" to="." method="_on_add_pair_pressed"] diff --git a/demo/scenes/realtime_db.gd b/demo/scenes/realtime_db.gd index dddb4d8..117c0f5 100644 --- a/demo/scenes/realtime_db.gd +++ b/demo/scenes/realtime_db.gd @@ -1,6 +1,7 @@ extends Control -@onready var path = $MarginContainer/VBoxContainer/HBoxContainer/path +@onready var output_panel = $MarginContainer/VBoxContainer/OutputPanel +@onready var path = $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer/path @onready var pair_container = $ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container func _notification(what: int) -> void: @@ -15,6 +16,12 @@ func _ready() -> void: Firebase.realtimeDB.delete_task_completed.connect(print_output.bind("delete_task_completed")) Firebase.realtimeDB.db_value_changed.connect(print_listner_output.bind("db_value_changed")) + +func _log(message: String) -> void: + var time = Time.get_time_string_from_system() + output_panel.text += "[%s] %s\n" % [time, message] + + func get_dictionary_from_inputs() -> Dictionary: var data_dict := Dictionary() for pair in pair_container.get_children(): @@ -27,11 +34,17 @@ func get_dictionary_from_inputs() -> Dictionary: data_dict[key] = value return data_dict + func print_output(arg, context: String): - $MarginContainer/VBoxContainer/OutputPanel.text += context + ": " +str(arg) + "\n" + _log(context + ": " + str(arg)) + func print_listner_output(arg, arg2, context: String): - $MarginContainer/VBoxContainer/OutputPanel.text += context + ": " +str(arg) + " -|- " +str(arg2) + "\n" + _log(context + ": " + str(arg) + " -|- " + str(arg2)) + + +func _on_clear_output_pressed() -> void: + output_panel.text = "" func _on_set_value_pressed() -> void: @@ -58,7 +71,6 @@ func _on_stop_listening_pressed() -> void: Firebase.realtimeDB.stop_listening(path.text) - func _on_manage_data_pressed() -> void: $ManageDataPanel.show() diff --git a/demo/scenes/realtime_db.tscn b/demo/scenes/realtime_db.tscn index 720873e..f4ae5ae 100644 --- a/demo/scenes/realtime_db.tscn +++ b/demo/scenes/realtime_db.tscn @@ -1,20 +1,9 @@ -[gd_scene load_steps=13 format=3 uid="uid://c55qaxgt7rib8"] +[gd_scene format=3 uid="uid://c55qaxgt7rib8"] [ext_resource type="Script" uid="uid://d2e4rd8p0lrkq" path="res://scenes/realtime_db.gd" id="1_t2rp6"] +[ext_resource type="Script" uid="uid://c1lwfy11opayr" path="res://scenes/scroll_container.gd" id="2_scroll"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1ykmf"] -draw_center = false -border_width_left = 3 -border_width_top = 3 -border_width_right = 3 -border_width_bottom = 3 -border_color = Color(0.14902, 0.117647, 0.854902, 1) -corner_radius_top_left = 20 -corner_radius_top_right = 20 -corner_radius_bottom_right = 20 -corner_radius_bottom_left = 20 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3kwyk"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hfmmi"] bg_color = Color(0.205117, 0.205117, 0.205117, 0.666667) corner_radius_top_left = 20 corner_radius_top_right = 20 @@ -28,14 +17,14 @@ corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hfmmi"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3kwyk"] bg_color = Color(0.205117, 0.205117, 0.205117, 0.666667) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7dm0k"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1ykmf"] draw_center = false border_width_left = 3 border_width_top = 3 @@ -61,6 +50,18 @@ corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7dm0k"] +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.14902, 0.117647, 0.854902, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bky7l"] content_margin_left = 20.0 content_margin_top = 20.0 @@ -72,33 +73,33 @@ corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_06hu1"] -draw_center = false -border_width_left = 3 -border_width_top = 3 -border_width_right = 3 -border_width_bottom = 3 -border_color = Color(0.14902, 0.117647, 0.854902, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q5sw8"] +bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q5sw8"] -bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bmyp4"] +bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bmyp4"] -bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_06hu1"] +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.14902, 0.117647, 0.854902, 1) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[node name="RealtimeDatabase" type="Control"] +[node name="RealtimeDatabase" type="Control" unique_id=1149314422] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -107,7 +108,7 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_t2rp6") -[node name="ColorRect" type="ColorRect" parent="."] +[node name="ColorRect" type="ColorRect" parent="." unique_id=1472682201] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -116,7 +117,7 @@ grow_horizontal = 2 grow_vertical = 2 color = Color(0.133333, 0.133333, 0.133333, 1) -[node name="MarginContainer" type="MarginContainer" parent="."] +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=667509518] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -128,99 +129,120 @@ theme_override_constants/margin_top = 20 theme_override_constants/margin_right = 40 theme_override_constants/margin_bottom = 20 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=1994767781] layout_mode = 2 -theme_override_constants/separation = 36 -alignment = 1 +theme_override_constants/separation = 10 -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer" unique_id=1887198070] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 +script = ExtResource("2_scroll") + +[node name="ButtonContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer" unique_id=1132883327] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 20 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=2049544354] custom_minimum_size = Vector2(0, 60) layout_mode = 2 theme_override_constants/separation = 30 -[node name="path" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"] +[node name="path" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/HBoxContainer" unique_id=962663907] layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 placeholder_text = "path" alignment = 1 -[node name="manage_data" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="manage_data" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1053021934] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_1ykmf") -theme_override_styles/hover = SubResource("StyleBoxFlat_3kwyk") -theme_override_styles/pressed = SubResource("StyleBoxFlat_iw5n2") theme_override_styles/normal = SubResource("StyleBoxFlat_hfmmi") +theme_override_styles/pressed = SubResource("StyleBoxFlat_iw5n2") +theme_override_styles/hover = SubResource("StyleBoxFlat_3kwyk") +theme_override_styles/focus = SubResource("StyleBoxFlat_1ykmf") text = "Manage Data" -[node name="set_value" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="set_value" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1938012268] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Set Value" -[node name="get_value" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="get_value" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=923445024] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Get Value" -[node name="update_value" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="update_value" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=918198287] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Update Value" -[node name="delete_value" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="delete_value" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=113401166] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Delete Value" -[node name="listen_to_path" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="listen_to_path" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=358988114] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Listen To Path" -[node name="stop_listening" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="stop_listening" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1112532937] custom_minimum_size = Vector2(120, 70) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") -theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") -theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") text = "Stop Listening" -[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"] +[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer" unique_id=740644201] +custom_minimum_size = Vector2(0, 200) layout_mode = 2 -size_flags_vertical = 3 -theme_override_font_sizes/normal_font_size = 32 +theme_override_font_sizes/normal_font_size = 24 +scroll_following = true + +[node name="clear_output" type="Button" parent="MarginContainer/VBoxContainer" unique_id=226045798] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +theme_override_styles/normal = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0xm2m") +theme_override_styles/hover = SubResource("StyleBoxFlat_ig7tw") +theme_override_styles/focus = SubResource("StyleBoxFlat_7dm0k") +text = "Clear Output" -[node name="ManageDataPanel" type="PanelContainer" parent="."] +[node name="ManageDataPanel" type="PanelContainer" parent="." unique_id=213862790] visible = false layout_mode = 1 anchors_preset = 8 @@ -236,24 +258,24 @@ grow_horizontal = 2 grow_vertical = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_bky7l") -[node name="VBoxContainer" type="VBoxContainer" parent="ManageDataPanel"] +[node name="VBoxContainer" type="VBoxContainer" parent="ManageDataPanel" unique_id=42376602] layout_mode = 2 theme_override_constants/separation = 50 -[node name="ScrollContainer" type="ScrollContainer" parent="ManageDataPanel/VBoxContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="ManageDataPanel/VBoxContainer" unique_id=1169353216] layout_mode = 2 size_flags_vertical = 3 -[node name="key_value_pair_container" type="VBoxContainer" parent="ManageDataPanel/VBoxContainer/ScrollContainer"] +[node name="key_value_pair_container" type="VBoxContainer" parent="ManageDataPanel/VBoxContainer/ScrollContainer" unique_id=989039928] layout_mode = 2 size_flags_horizontal = 3 -[node name="sample_pair" type="HBoxContainer" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container"] +[node name="sample_pair" type="HBoxContainer" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container" unique_id=599830394] visible = false layout_mode = 2 theme_override_constants/separation = 30 -[node name="key" type="LineEdit" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair"] +[node name="key" type="LineEdit" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair" unique_id=228169842] custom_minimum_size = Vector2(0, 60) layout_mode = 2 size_flags_horizontal = 3 @@ -261,7 +283,7 @@ theme_override_font_sizes/font_size = 32 placeholder_text = "key" alignment = 1 -[node name="value" type="LineEdit" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair"] +[node name="value" type="LineEdit" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair" unique_id=1267595680] custom_minimum_size = Vector2(0, 60) layout_mode = 2 size_flags_horizontal = 3 @@ -269,44 +291,45 @@ theme_override_font_sizes/font_size = 32 placeholder_text = "values" alignment = 1 -[node name="CheckBox" type="CheckButton" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair"] +[node name="CheckBox" type="CheckButton" parent="ManageDataPanel/VBoxContainer/ScrollContainer/key_value_pair_container/sample_pair" unique_id=419429823] layout_mode = 2 theme_override_font_sizes/font_size = 32 text = "Numeric" -[node name="HBoxContainer" type="HBoxContainer" parent="ManageDataPanel/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="ManageDataPanel/VBoxContainer" unique_id=239544796] layout_mode = 2 theme_override_constants/separation = 30 alignment = 1 -[node name="close" type="Button" parent="ManageDataPanel/VBoxContainer/HBoxContainer"] +[node name="close" type="Button" parent="ManageDataPanel/VBoxContainer/HBoxContainer" unique_id=1741717187] custom_minimum_size = Vector2(120, 70) layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_06hu1") -theme_override_styles/hover = SubResource("StyleBoxFlat_q5sw8") -theme_override_styles/pressed = SubResource("StyleBoxFlat_bmyp4") theme_override_styles/normal = SubResource("StyleBoxFlat_q5sw8") +theme_override_styles/pressed = SubResource("StyleBoxFlat_bmyp4") +theme_override_styles/hover = SubResource("StyleBoxFlat_q5sw8") +theme_override_styles/focus = SubResource("StyleBoxFlat_06hu1") text = "Close" -[node name="add_pair" type="Button" parent="ManageDataPanel/VBoxContainer/HBoxContainer"] +[node name="add_pair" type="Button" parent="ManageDataPanel/VBoxContainer/HBoxContainer" unique_id=2120634655] custom_minimum_size = Vector2(120, 70) layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_06hu1") -theme_override_styles/hover = SubResource("StyleBoxFlat_q5sw8") -theme_override_styles/pressed = SubResource("StyleBoxFlat_bmyp4") theme_override_styles/normal = SubResource("StyleBoxFlat_q5sw8") +theme_override_styles/pressed = SubResource("StyleBoxFlat_bmyp4") +theme_override_styles/hover = SubResource("StyleBoxFlat_q5sw8") +theme_override_styles/focus = SubResource("StyleBoxFlat_06hu1") text = "New Pair" -[connection signal="pressed" from="MarginContainer/VBoxContainer/manage_data" to="." method="_on_manage_data_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/set_value" to="." method="_on_set_value_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/get_value" to="." method="_on_get_value_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/update_value" to="." method="_on_update_value_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/delete_value" to="." method="_on_delete_value_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/listen_to_path" to="." method="_on_listen_to_path_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/stop_listening" to="." method="_on_stop_listening_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/manage_data" to="." method="_on_manage_data_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_value" to="." method="_on_set_value_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_value" to="." method="_on_get_value_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/update_value" to="." method="_on_update_value_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/delete_value" to="." method="_on_delete_value_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/listen_to_path" to="." method="_on_listen_to_path_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/stop_listening" to="." method="_on_stop_listening_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/clear_output" to="." method="_on_clear_output_pressed"] [connection signal="pressed" from="ManageDataPanel/VBoxContainer/HBoxContainer/close" to="." method="_on_close_pressed"] [connection signal="pressed" from="ManageDataPanel/VBoxContainer/HBoxContainer/add_pair" to="." method="_on_add_pair_pressed"] diff --git a/demo/scenes/remote_config.gd b/demo/scenes/remote_config.gd new file mode 100644 index 0000000..bf0c818 --- /dev/null +++ b/demo/scenes/remote_config.gd @@ -0,0 +1,125 @@ +extends Control + +@onready var output: RichTextLabel = %OutputPanel +@onready var key_input: LineEdit = %KeyInput + +func _notification(what: int) -> void: + if what == NOTIFICATION_WM_GO_BACK_REQUEST: + get_tree().change_scene_to_packed(load("res://main.tscn")) + +func _ready() -> void: + Firebase.remote_config.fetch_completed.connect(_on_fetch_completed) + Firebase.remote_config.activate_completed.connect(_on_activate_completed) + Firebase.remote_config.config_updated.connect(_on_config_updated) + _log("platform", "Android") + +# --- Signal handlers --- + +func _on_fetch_completed(result: Dictionary) -> void: + _log("fetch", "status=%s activated=%s error=%s" % [result.get("status"), result.get("activated", ""), result.get("error", "")]) + +func _on_activate_completed(result: Dictionary) -> void: + _log("activate", "status=%s activated=%s error=%s" % [result.get("status"), result.get("activated", ""), result.get("error", "")]) + +func _on_config_updated(updated_keys) -> void: + _log("update", "keys=%s" % str(updated_keys)) + +# --- Setup --- + +func _on_initialize_pressed() -> void: + _log("action", "Initializing Remote Config...") + Firebase.remote_config.initialize() + _log("done", "Remote Config initialized") + +func _on_set_defaults_pressed() -> void: + var defaults = {"welcome_message": "Hello!", "feature_enabled": false, "max_items": 10} + _log("action", "Setting defaults: %s" % JSON.stringify(defaults)) + Firebase.remote_config.set_defaults(defaults) + _log("done", "Defaults set") + +func _on_set_dev_interval_pressed() -> void: + _log("action", "Setting minimum fetch interval to 0 (dev mode)") + Firebase.remote_config.set_minimum_fetch_interval(0) + +func _on_set_fetch_timeout_pressed() -> void: + _log("action", "Setting fetch timeout to 10s") + Firebase.remote_config.set_fetch_timeout(10) + +# --- Fetch operations --- + +func _on_fetch_pressed() -> void: + _log("action", "Fetching remote config...") + Firebase.remote_config.fetch() + +func _on_activate_pressed() -> void: + _log("action", "Activating fetched config...") + Firebase.remote_config.activate() + +func _on_fetch_and_activate_pressed() -> void: + _log("action", "Fetching and activating...") + Firebase.remote_config.fetch_and_activate() + +# --- Read values --- + +func _on_get_value_pressed() -> void: + var key = key_input.text + if key.is_empty(): + _log("error", "Enter a key name first") + return + var str_val = Firebase.remote_config.get_string(key) + var bool_val = Firebase.remote_config.get_bool(key) + var int_val = Firebase.remote_config.get_int(key) + var float_val = Firebase.remote_config.get_float(key) + _log("value", "key='%s' string='%s' bool=%s int=%s float=%s" % [key, str_val, bool_val, int_val, float_val]) + +func _on_get_all_pressed() -> void: + var all_values = Firebase.remote_config.get_all() + _log("all", JSON.stringify(all_values)) + +func _on_get_json_pressed() -> void: + var key = key_input.text + if key.is_empty(): + _log("error", "Enter a key name first") + return + var json_val = Firebase.remote_config.get_json(key) + _log("json", "key='%s' json='%s'" % [key, json_val]) + +func _on_get_source_pressed() -> void: + var key = key_input.text + if key.is_empty(): + _log("error", "Enter a key name first") + return + var source = Firebase.remote_config.get_value_source(key) + var source_names = {0: "static", 1: "default", 2: "remote"} + _log("source", "key='%s' source=%s" % [key, source_names.get(source, "unknown")]) + +func _on_fetch_status_pressed() -> void: + var status = Firebase.remote_config.get_last_fetch_status() + var status_names = {-1: "success", 0: "no_fetch_yet", 1: "failure", 2: "throttled"} + var fetch_time = Firebase.remote_config.get_last_fetch_time() + _log("status", "last_fetch=%s time=%s" % [status_names.get(status, "unknown"), fetch_time]) + +# --- Real-time updates --- + +func _on_listen_pressed() -> void: + _log("action", "Listening for config updates...") + Firebase.remote_config.listen_for_updates() + +func _on_stop_listen_pressed() -> void: + _log("action", "Stopped listening for config updates") + Firebase.remote_config.stop_listening_for_updates() + +# --- Logging --- + +func _log(context: String, message: String) -> void: + var t = Time.get_time_string_from_system() + output.text += "[%s] %s: %s\n" % [t, context, message] + if not is_inside_tree(): + return + await get_tree().process_frame + if not is_inside_tree(): + return + output.scroll_to_line(output.get_line_count()) + +func _on_clear_output_pressed() -> void: + output.text = "" diff --git a/demo/scenes/remote_config.gd.uid b/demo/scenes/remote_config.gd.uid new file mode 100644 index 0000000..7eb3440 --- /dev/null +++ b/demo/scenes/remote_config.gd.uid @@ -0,0 +1 @@ +uid://cxjy6y3m13nqt diff --git a/demo/scenes/remote_config.tscn b/demo/scenes/remote_config.tscn new file mode 100644 index 0000000..8d9b9a4 --- /dev/null +++ b/demo/scenes/remote_config.tscn @@ -0,0 +1,256 @@ +[gd_scene format=3 uid="uid://cjgelvmfrh42v"] + +[ext_resource type="Script" uid="uid://cxjy6y3m13nqt" path="res://scenes/remote_config.gd" id="1_remote_config"] +[ext_resource type="Script" uid="uid://c1lwfy11opayr" path="res://scenes/scroll_container.gd" id="2_scroll"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_normal"] +bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pressed"] +bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_focus"] +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.14902, 0.117647, 0.854902, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[node name="RemoteConfig" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_remote_config") + +[node name="ColorRect" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.133333, 0.133333, 0.133333, 1) + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 40 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 40 +theme_override_constants/margin_bottom = 20 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 +script = ExtResource("2_scroll") + +[node name="ButtonContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 20 + +[node name="initialize" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Initialize" + +[node name="set_defaults" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Set Defaults" + +[node name="set_dev_interval" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Set Dev Interval (0s)" + +[node name="set_fetch_timeout" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Set Fetch Timeout (10s)" + +[node name="fetch" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Fetch" + +[node name="activate" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Activate" + +[node name="fetch_and_activate" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Fetch & Activate" + +[node name="KeyInput" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "welcome_message" +placeholder_text = "Config key" + +[node name="get_value" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Get Value" + +[node name="get_all" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Get All Values" + +[node name="get_json" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Get JSON Value" + +[node name="get_source" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Get Value Source" + +[node name="fetch_status" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Fetch Status" + +[node name="listen" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Listen for Updates" + +[node name="stop_listen" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Stop Listening" + +[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 200) +layout_mode = 2 +theme_override_font_sizes/normal_font_size = 24 +scroll_following = true + +[node name="clear_output" type="Button" parent="MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +theme_override_styles/normal = SubResource("StyleBoxFlat_normal") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/hover = SubResource("StyleBoxFlat_normal") +theme_override_styles/focus = SubResource("StyleBoxFlat_focus") +text = "Clear Output" + +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/initialize" to="." method="_on_initialize_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_defaults" to="." method="_on_set_defaults_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_dev_interval" to="." method="_on_set_dev_interval_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/set_fetch_timeout" to="." method="_on_set_fetch_timeout_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/fetch" to="." method="_on_fetch_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/activate" to="." method="_on_activate_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/fetch_and_activate" to="." method="_on_fetch_and_activate_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_value" to="." method="_on_get_value_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_all" to="." method="_on_get_all_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_json" to="." method="_on_get_json_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_source" to="." method="_on_get_source_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/fetch_status" to="." method="_on_fetch_status_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/listen" to="." method="_on_listen_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/stop_listen" to="." method="_on_stop_listen_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/clear_output" to="." method="_on_clear_output_pressed"] diff --git a/demo/scenes/scroll_container.gd b/demo/scenes/scroll_container.gd new file mode 100644 index 0000000..bde2d6b --- /dev/null +++ b/demo/scenes/scroll_container.gd @@ -0,0 +1,37 @@ +extends ScrollContainer + +var swiping = false +var swipe_start: Vector2 +var swipe_mouse_start: Vector2 +var is_enabled: bool = true + +func _input(ev): + if is_enabled: + if self.get_global_rect().has_point(get_global_mouse_position()): + if ev is InputEventMouseButton: + if ev.pressed: + swip_controller(true) + swipe_start = Vector2(get_h_scroll(), get_v_scroll()) + swipe_mouse_start = ev.position + else: + swip_controller(false) + elif swiping and ev is InputEventMouseMotion: + var delta = ev.position - swipe_mouse_start + if horizontal_scroll_mode != ScrollMode.SCROLL_MODE_DISABLED: + set_h_scroll(swipe_start.x - delta.x) + if vertical_scroll_mode != ScrollMode.SCROLL_MODE_DISABLED: + set_v_scroll(swipe_start.y - delta.y) + else: + swip_controller(false) + +func swip_controller(is_swiping) -> void: + swiping = is_swiping + +func swip_enabler(p_is_enabled: bool) -> void: + mouse_filter = Control.MOUSE_FILTER_PASS + vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO + is_enabled = p_is_enabled + swip_controller(false) + if not is_enabled: + vertical_scroll_mode = ScrollContainer.SCROLL_MODE_SHOW_NEVER + mouse_filter = Control.MOUSE_FILTER_IGNORE diff --git a/demo/scenes/scroll_container.gd.uid b/demo/scenes/scroll_container.gd.uid new file mode 100644 index 0000000..1ad079f --- /dev/null +++ b/demo/scenes/scroll_container.gd.uid @@ -0,0 +1 @@ +uid://c1lwfy11opayr diff --git a/demo/scenes/storage.gd b/demo/scenes/storage.gd index f1bc3fa..2420dea 100644 --- a/demo/scenes/storage.gd +++ b/demo/scenes/storage.gd @@ -2,8 +2,8 @@ extends Control @onready var output_panel = $MarginContainer/VBoxContainer/OutputPanel -@onready var download_path = $MarginContainer/VBoxContainer/LineEdit2 -@onready var cloud_storage_path = $MarginContainer/VBoxContainer/LineEdit3 +@onready var download_path = $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/LineEdit2 +@onready var cloud_storage_path = $MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/LineEdit3 var selected_image_path: String @@ -11,6 +11,7 @@ func _notification(what: int) -> void: if what == NOTIFICATION_WM_GO_BACK_REQUEST: get_tree().change_scene_to_packed(load("res://main.tscn")) + func _ready() -> void: Firebase.storage.upload_task_completed.connect(print_output.bind("upload_task_completed")) Firebase.storage.download_task_completed.connect(print_output.bind("download_task_completed")) @@ -18,11 +19,22 @@ func _ready() -> void: Firebase.storage.list_task_completed.connect(print_output.bind("list_task_completed")) OS.request_permissions() -func _on_get_image_path_pressed() -> void: - $FileDialog.popup() + +func _log(message: String) -> void: + var time = Time.get_time_string_from_system() + output_panel.text += "[%s] %s\n" % [time, message] + func print_output(arg, context: String): - $MarginContainer/VBoxContainer/OutputPanel.text += context + ": " +str(arg) + "\n" + _log(context + ": " + str(arg)) + + +func _on_clear_output_pressed() -> void: + output_panel.text = "" + + +func _on_get_image_path_pressed() -> void: + $FileDialog.popup() func _on_upload_file_pressed() -> void: @@ -46,7 +58,7 @@ func _on_list_files_pressed() -> void: func _on_file_dialog_file_selected(path: String) -> void: - print(path) + _log("Selected: " + path) selected_image_path = path diff --git a/demo/scenes/storage.tscn b/demo/scenes/storage.tscn index e31d341..5eaa61b 100644 --- a/demo/scenes/storage.tscn +++ b/demo/scenes/storage.tscn @@ -1,34 +1,35 @@ -[gd_scene load_steps=5 format=3 uid="uid://cpxjrj5th1sdt"] +[gd_scene format=3 uid="uid://cpxjrj5th1sdt"] [ext_resource type="Script" uid="uid://bqo5rpk2iaqo3" path="res://scenes/storage.gd" id="1_ueev3"] +[ext_resource type="Script" uid="uid://c1lwfy11opayr" path="res://scenes/scroll_container.gd" id="2_scroll"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nnb4j"] -draw_center = false -border_width_left = 3 -border_width_top = 3 -border_width_right = 3 -border_width_bottom = 3 -border_color = Color(0.14902, 0.117647, 0.854902, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ykan5"] +bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ykan5"] -bg_color = Color(0.180392, 0.596078, 0.572549, 0.666667) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_taf4q"] +bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_taf4q"] -bg_color = Color(0.14902, 0.117647, 0.854902, 0.564706) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nnb4j"] +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.14902, 0.117647, 0.854902, 1) corner_radius_top_left = 20 corner_radius_top_right = 20 corner_radius_bottom_right = 20 corner_radius_bottom_left = 20 -[node name="Storage" type="Control"] +[node name="Storage" type="Control" unique_id=65314000] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -37,7 +38,7 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_ueev3") -[node name="ColorRect" type="ColorRect" parent="."] +[node name="ColorRect" type="ColorRect" parent="." unique_id=1362240815] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -46,7 +47,7 @@ grow_horizontal = 2 grow_vertical = 2 color = Color(0.133333, 0.133333, 0.133333, 1) -[node name="MarginContainer" type="MarginContainer" parent="."] +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1689403991] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -58,95 +59,116 @@ theme_override_constants/margin_top = 20 theme_override_constants/margin_right = 40 theme_override_constants/margin_bottom = 20 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=989591773] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer" unique_id=1561854334] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 +script = ExtResource("2_scroll") + +[node name="ButtonContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer" unique_id=1183596626] layout_mode = 2 -theme_override_constants/separation = 36 -alignment = 1 +size_flags_horizontal = 3 +theme_override_constants/separation = 20 -[node name="get_image_path" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="get_image_path" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=687392687] custom_minimum_size = Vector2(0, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 text = "select an Image to upload" -[node name="LineEdit2" type="LineEdit" parent="MarginContainer/VBoxContainer"] +[node name="LineEdit2" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=783437278] custom_minimum_size = Vector2(0, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 placeholder_text = "download file path (local storage)" -[node name="LineEdit3" type="LineEdit" parent="MarginContainer/VBoxContainer"] +[node name="LineEdit3" type="LineEdit" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1897142306] custom_minimum_size = Vector2(0, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 placeholder_text = "file path (Cloud Storage)" -[node name="upload_file" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="upload_file" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1376444288] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") -theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") -theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") theme_override_styles/normal = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") +theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") text = "Upload File" -[node name="download_url" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="download_url" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=442619606] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") -theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") -theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") theme_override_styles/normal = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") +theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") text = "Get Download URL" -[node name="download_file" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="download_file" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=780448160] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") -theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") -theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") theme_override_styles/normal = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") +theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") text = "Download File" -[node name="get_metadata" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="get_metadata" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=180340275] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") -theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") -theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") theme_override_styles/normal = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") +theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") text = "Get Metadata" -[node name="delete_file" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="delete_file" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1140412495] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") -theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") -theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") theme_override_styles/normal = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") +theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") text = "Delete File" -[node name="list_files" type="Button" parent="MarginContainer/VBoxContainer"] +[node name="list_files" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer" unique_id=1456738704] custom_minimum_size = Vector2(120, 60) layout_mode = 2 theme_override_font_sizes/font_size = 32 -theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") -theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") -theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") theme_override_styles/normal = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") +theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") text = "List Files" -[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"] +[node name="OutputPanel" type="RichTextLabel" parent="MarginContainer/VBoxContainer" unique_id=848450197] +custom_minimum_size = Vector2(0, 200) layout_mode = 2 -size_flags_vertical = 3 -theme_override_font_sizes/normal_font_size = 32 +theme_override_font_sizes/normal_font_size = 24 +scroll_following = true + +[node name="clear_output" type="Button" parent="MarginContainer/VBoxContainer" unique_id=1878858443] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +theme_override_styles/normal = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_taf4q") +theme_override_styles/hover = SubResource("StyleBoxFlat_ykan5") +theme_override_styles/focus = SubResource("StyleBoxFlat_nnb4j") +text = "Clear Output" -[node name="FileDialog" type="FileDialog" parent="."] +[node name="FileDialog" type="FileDialog" parent="." unique_id=98124820] title = "Open a File" position = Vector2i(0, 36) ok_button_text = "Open" @@ -155,11 +177,12 @@ access = 2 filters = PackedStringArray("image/*") use_native_dialog = true -[connection signal="pressed" from="MarginContainer/VBoxContainer/get_image_path" to="." method="_on_get_image_path_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/upload_file" to="." method="_on_upload_file_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/download_url" to="." method="_on_download_url_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/download_file" to="." method="_on_download_file_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/get_metadata" to="." method="_on_get_metadata_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/delete_file" to="." method="_on_delete_file_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/list_files" to="." method="_on_list_files_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_image_path" to="." method="_on_get_image_path_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/upload_file" to="." method="_on_upload_file_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/download_url" to="." method="_on_download_url_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/download_file" to="." method="_on_download_file_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/get_metadata" to="." method="_on_get_metadata_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/delete_file" to="." method="_on_delete_file_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/ScrollContainer/ButtonContainer/list_files" to="." method="_on_list_files_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/clear_output" to="." method="_on_clear_output_pressed"] [connection signal="file_selected" from="FileDialog" to="." method="_on_file_dialog_file_selected"] diff --git a/docs/analytics.md b/docs/analytics.md new file mode 100644 index 0000000..0e5f4c3 --- /dev/null +++ b/docs/analytics.md @@ -0,0 +1,128 @@ +--- +layout: default +title: Analytics +nav_order: 6 +--- + +# Firebase Analytics + +The Firebase Analytics module in **GodotFirebaseAndroid** allows you to log events, set user properties, manage consent, and retrieve the app instance ID. + +## Signals + +- `app_instance_id_result(id: String)` + Emitted after requesting the app instance ID. Returns an empty string on failure. + +## Methods + +{: .text-green-100 } +### log_event(name: String, parameters: Dictionary) + +Logs a custom analytics event. Parameter values can be `String`, `int`, `float`, or `bool`. + +```gdscript +Firebase.analytics.log_event("level_complete", {"level": 5, "score": 1200}) +``` + +--- + +{: .text-green-100 } +### set_user_property(name: String, value: String) + +Sets a user property for analytics segmentation. Up to 25 user properties can be set per project. + +```gdscript +Firebase.analytics.set_user_property("favorite_food", "pizza") +``` + +--- + +{: .text-green-100 } +### set_user_id(id: String) + +Sets the user ID for analytics. This is useful for linking analytics data to your own user system. + +```gdscript +Firebase.analytics.set_user_id("user_123") +``` + +--- + +{: .text-green-100 } +### set_analytics_collection_enabled(enabled: bool) + +Enables or disables analytics data collection. When disabled, no events are logged. + +```gdscript +Firebase.analytics.set_analytics_collection_enabled(false) # opt out +``` + +--- + +{: .text-green-100 } +### reset_analytics_data() + +Clears all analytics data from the device and resets the app instance ID. + +```gdscript +Firebase.analytics.reset_analytics_data() +``` + +--- + +{: .text-green-100 } +### set_default_event_parameters(parameters: Dictionary) + +Sets default parameters that are sent with every subsequent event. Useful for values like app version or user segment that apply to all events. + +```gdscript +Firebase.analytics.set_default_event_parameters({"app_version": "1.2.0", "platform": "android"}) +``` + +--- + +{: .text-green-100 } +### get_app_instance_id() + +Requests the unique app instance ID assigned by Firebase Analytics. The result is returned asynchronously via signal. + +**Emits:** `app_instance_id_result` + +```gdscript +Firebase.analytics.get_app_instance_id() +# Listen for the result: +# Firebase.analytics.app_instance_id_result.connect(_on_instance_id) +``` + +--- + +{: .text-green-100 } +### set_consent(ad_storage: bool, analytics_storage: bool, ad_user_data: bool, ad_personalization: bool) + +Sets the consent status for various data collection purposes. Use this to comply with consent requirements (e.g., GDPR). + +| Parameter | Purpose | +|---|---| +| `ad_storage` | Enables storage of advertising-related data | +| `analytics_storage` | Enables storage of analytics-related data | +| `ad_user_data` | Allows sending user data to Google for advertising | +| `ad_personalization` | Allows personalized advertising | + +```gdscript +# Grant all consent +Firebase.analytics.set_consent(true, true, true, true) + +# Deny advertising, allow analytics +Firebase.analytics.set_consent(false, true, false, false) +``` + +--- + +{: .text-green-100 } +### set_session_timeout(seconds: int) + +Sets the duration of inactivity (in seconds) before a session expires. Default is 1800 seconds (30 minutes). + +```gdscript +Firebase.analytics.set_session_timeout(600) # 10 minutes +``` diff --git a/docs/authentication.md b/docs/authentication.md index bc9ccd9..945e54a 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -34,6 +34,21 @@ The Firebase Authentication module in **GodotFirebaseAndroid** supports anonymou - `user_deleted(success: bool)` Emitted after an attempt to delete the current user. +- `auth_state_changed(signed_in: bool, current_user_data: Dictionary)` + Emitted when the user's authentication state changes (sign in or sign out). Fires immediately when the listener is added with the current state. + +- `id_token_result(token: String)` + Emitted when an ID token is successfully retrieved. + +- `id_token_error(error_message: String)` + Emitted when retrieving an ID token fails. + +- `profile_updated(success: bool)` + Emitted when a user profile update succeeds. + +- `profile_update_failure(error_message: String)` + Emitted when a user profile update fails. + ## Methods {: .text-green-100 } @@ -173,3 +188,121 @@ Signs out the current user. ```gdscript Firebase.auth.sign_out() ``` + +--- + +{: .text-green-100 } +### use_emulator(host: String, port: int) + +Connects the Auth module to a local Firebase Auth emulator. Must be called before any other Auth operations. + +{: .warning } +Only use this during development. Do not ship with emulator enabled. + +```gdscript +Firebase.auth.use_emulator("10.0.2.2", 9099) +``` + +--- + +{: .text-green-100 } +### reauthenticate_with_email(email: String, password: String) + +Reauthenticates the current user with email and password credentials. Required before sensitive operations like `update_password` or `delete_current_user` if the user's last sign-in was too long ago. + +**Emits:** `auth_success` or `auth_failure`. + +```gdscript +Firebase.auth.reauthenticate_with_email("testuser@email.com", "password123") +``` + +--- + +{: .text-green-100 } +### add_auth_state_listener() + +Starts listening for authentication state changes. The listener fires immediately with the current state, then again whenever the user signs in or out. + +**Emits:** `auth_state_changed` on every state change. + +```gdscript +Firebase.auth.add_auth_state_listener() +``` + +--- + +{: .text-green-100 } +### remove_auth_state_listener() + +Stops listening for authentication state changes. + +```gdscript +Firebase.auth.remove_auth_state_listener() +``` + +--- + +{: .text-green-100 } +### get_id_token(force_refresh: bool = false) + +Retrieves the Firebase ID token for the current user. The token can be used to authenticate with your backend server. Set `force_refresh` to `true` to force a token refresh even if the current token hasn't expired. + +**Emits:** `id_token_result` or `id_token_error`. + +```gdscript +Firebase.auth.get_id_token() # use cached token +Firebase.auth.get_id_token(true) # force refresh +``` + +--- + +{: .text-green-100 } +### update_profile(display_name: String, photo_url: String = "") + +Updates the current user's display name and/or photo URL. Pass an empty string to leave a field unchanged. + +**Emits:** `profile_updated` or `profile_update_failure`. + +```gdscript +Firebase.auth.update_profile("Alice", "https://example.com/photo.png") +Firebase.auth.update_profile("Alice") # update name only +``` + +--- + +{: .text-green-100 } +### update_password(new_password: String) + +Updates the current user's password. The user must have been recently authenticated (see `reauthenticate_with_email`). + +**Emits:** `auth_success` or `auth_failure`. + +```gdscript +Firebase.auth.update_password("newSecurePassword123") +``` + +--- + +{: .text-green-100 } +### reload_user() + +Reloads the current user's profile data from Firebase. Useful after operations like email verification to get the updated `emailVerified` status. + +**Emits:** `auth_success` or `auth_failure`. + +```gdscript +Firebase.auth.reload_user() +``` + +--- + +{: .text-green-100 } +### unlink_provider(provider_id: String) + +Unlinks a provider from the current user's account. Common provider IDs: `"google.com"`, `"password"`. + +**Emits:** `auth_success` or `auth_failure`. + +```gdscript +Firebase.auth.unlink_provider("google.com") +``` diff --git a/docs/firestore.md b/docs/firestore.md index ed56bb2..89e6f4c 100644 --- a/docs/firestore.md +++ b/docs/firestore.md @@ -25,6 +25,18 @@ The Cloud Firestore module in **GodotFirebaseAndroid** supports adding, retrievi - `document_changed(document_path: String, data: Dictionary)` Emitted when a listened document is changed. +- `query_task_completed(result: Dictionary)` + Emitted after a query operation completes. Contains a `documents` array of results. + +- `collection_changed(collection_path: String, documents: Array)` + Emitted when any document in a listened collection changes. Each element in the array is a dictionary with `docID` and `data` keys. + +- `batch_task_completed(result: Dictionary)` + Emitted after a WriteBatch commit completes. + +- `transaction_task_completed(result: Dictionary)` + Emitted after a transaction completes. + **Note**: All signal result dictionaries contain the following keys: - status (bool): true if the operation succeeded, false otherwise. @@ -136,3 +148,209 @@ Stops listening to changes for a document path. ```gdscript Firebase.firestore.stop_listening_to_document("players/user_123") ``` + +--- + +{: .text-green-100 } +### query_documents(collection: String, filters: Array = [], order_by: String = "", order_descending: bool = false, limit_count: int = 0) + +Queries documents in a collection with optional filters, ordering, and limits. Each filter is a dictionary with `field`, `op`, and `value` keys. + +Supported operators: `==`, `!=`, `<`, `<=`, `>`, `>=`, `array_contains`, `in`, `not_in`, `array_contains_any`. + +**Emits:** `query_task_completed` + +```gdscript +var filters = [ + {"field": "score", "op": ">=", "value": 100}, + {"field": "active", "op": "==", "value": true} +] +Firebase.firestore.query_documents("players", filters, "score", true, 10) +``` + +--- + +{: .text-green-100 } +### use_emulator(host: String, port: int) + +Connects the Firestore module to a local Firebase Firestore emulator. Must be called before any other Firestore operations. + +{: .warning } +Only use this during development. Do not ship with emulator enabled. + +```gdscript +Firebase.firestore.use_emulator("10.0.2.2", 8080) +``` + +--- + +{: .text-green-100 } +### listen_to_collection(collection: String) + +Starts listening to all changes in a collection. When any document is added, modified, or removed, the signal fires with the full list of documents. + +**Emits:** `collection_changed` + +```gdscript +Firebase.firestore.listen_to_collection("players") +``` + +--- + +{: .text-green-100 } +### stop_listening_to_collection(collection: String) + +Stops listening to changes in a collection. + +```gdscript +Firebase.firestore.stop_listening_to_collection("players") +``` + +--- + +## WriteBatch Operations + +WriteBatch allows you to group multiple write operations into a single atomic commit. Either all operations succeed or none are applied. + +{: .text-green-100 } +### create_batch() -> int + +Creates a new WriteBatch and returns a batch ID used for subsequent batch operations. + +```gdscript +var batch_id = Firebase.firestore.create_batch() +``` + +--- + +{: .text-green-100 } +### batch_set(batch_id: int, collection: String, document_id: String, data: Dictionary, merge: bool = false) + +Adds a set operation to the batch. + +```gdscript +Firebase.firestore.batch_set(batch_id, "players", "user_1", {"name": "Alice", "score": 100}) +``` + +--- + +{: .text-green-100 } +### batch_update(batch_id: int, collection: String, document_id: String, data: Dictionary) + +Adds an update operation to the batch. The document must already exist. + +```gdscript +Firebase.firestore.batch_update(batch_id, "players", "user_1", {"score": 200}) +``` + +--- + +{: .text-green-100 } +### batch_delete(batch_id: int, collection: String, document_id: String) + +Adds a delete operation to the batch. + +```gdscript +Firebase.firestore.batch_delete(batch_id, "players", "user_old") +``` + +--- + +{: .text-green-100 } +### commit_batch(batch_id: int) + +Commits all operations in the batch atomically. + +**Emits:** `batch_task_completed` + +```gdscript +var batch_id = Firebase.firestore.create_batch() +Firebase.firestore.batch_set(batch_id, "players", "user_1", {"name": "Alice"}) +Firebase.firestore.batch_update(batch_id, "players", "user_2", {"score": 300}) +Firebase.firestore.batch_delete(batch_id, "players", "user_old") +Firebase.firestore.commit_batch(batch_id) +``` + +--- + +## Transactions + +{: .text-green-100 } +### run_transaction(collection: String, document_id: String, update_data: Dictionary) + +Runs a Firestore transaction that reads the document and then applies the update atomically. If another client modifies the document during the transaction, Firestore automatically retries. + +**Emits:** `transaction_task_completed` + +```gdscript +Firebase.firestore.run_transaction("players", "user_123", {"score": 500}) +``` + +--- + +## FieldValue Helpers + +Special values that can be used inside document data for atomic server-side operations. Use these as values in dictionaries passed to `add_document`, `set_document`, `update_document`, or batch operations. + +{: .text-green-100 } +### server_timestamp() -> Dictionary + +Returns a sentinel value that tells Firestore to use the server timestamp. + +```gdscript +Firebase.firestore.update_document("players", "user_123", { + "last_login": Firebase.firestore.server_timestamp() +}) +``` + +--- + +{: .text-green-100 } +### array_union(elements: Array) -> Dictionary + +Returns a sentinel value that adds elements to an array field without duplicates. + +```gdscript +Firebase.firestore.update_document("players", "user_123", { + "badges": Firebase.firestore.array_union(["gold", "silver"]) +}) +``` + +--- + +{: .text-green-100 } +### array_remove(elements: Array) -> Dictionary + +Returns a sentinel value that removes elements from an array field. + +```gdscript +Firebase.firestore.update_document("players", "user_123", { + "badges": Firebase.firestore.array_remove(["bronze"]) +}) +``` + +--- + +{: .text-green-100 } +### increment_by(value: float) -> Dictionary + +Returns a sentinel value that atomically increments a numeric field. + +```gdscript +Firebase.firestore.update_document("players", "user_123", { + "score": Firebase.firestore.increment_by(10) +}) +``` + +--- + +{: .text-green-100 } +### delete_field() -> Dictionary + +Returns a sentinel value that removes a field from a document. + +```gdscript +Firebase.firestore.update_document("players", "user_123", { + "deprecated_field": Firebase.firestore.delete_field() +}) +``` diff --git a/docs/index.md b/docs/index.md index e3f2539..9235a93 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,6 +14,8 @@ nav_order: 1 - ✅ Cloud Firestore - ✅ Realtime Database - ✅ Cloud Storage +- ✅ Firebase Analytics +- ✅ Remote Config - 🔜 Cloud Messaging (Coming Soon) ## Getting Started diff --git a/docs/remote_config.md b/docs/remote_config.md new file mode 100644 index 0000000..77def36 --- /dev/null +++ b/docs/remote_config.md @@ -0,0 +1,269 @@ +--- +layout: default +title: Remote Config +nav_order: 7 +--- + +# Remote Config + +The Remote Config module in **GodotFirebaseAndroid** lets you fetch and activate configuration values from the Firebase console, enabling you to change your app's behavior and appearance without publishing an update. + +## Signals + +- `fetch_completed(result: Dictionary)` + Emitted after a fetch or fetch-and-activate operation completes. The result dictionary contains: + - `status` (bool): `true` if the fetch succeeded + - `activated` (bool): `true` if new values were activated (only present for `fetch_and_activate` and `activate`) + - `error` (String): Error message if the operation failed + +- `activate_completed(result: Dictionary)` + Emitted after an activate operation completes. Same keys as `fetch_completed`. + +- `config_updated(updated_keys: Array)` + Emitted when config values are updated in real-time. Contains an array of the keys that changed. + +## Methods + +{: .text-green-100 } +### initialize() + +Initializes Remote Config with the current settings. Call this once at startup. + +```gdscript +Firebase.remote_config.initialize() +``` + +--- + +{: .text-green-100 } +### set_defaults(defaults: Dictionary) + +Sets default config values that are used before fetched values are available. + +```gdscript +Firebase.remote_config.set_defaults({ + "welcome_message": "Hello!", + "max_retries": 3, + "feature_enabled": true +}) +``` + +--- + +{: .text-green-100 } +### set_minimum_fetch_interval(seconds: int) + +Sets the minimum interval between fetch requests. During development, set this to `0` to fetch on every request. Default is 3600 seconds (1 hour). + +{: .note } +Firebase throttles fetch requests. In production, keep this at 3600 or higher to avoid throttling. + +```gdscript +Firebase.remote_config.set_minimum_fetch_interval(0) # development +Firebase.remote_config.set_minimum_fetch_interval(3600) # production +``` + +--- + +{: .text-green-100 } +### set_fetch_timeout(seconds: int) + +Sets the timeout for fetch requests. Default is 60 seconds. + +```gdscript +Firebase.remote_config.set_fetch_timeout(30) +``` + +--- + +{: .text-green-100 } +### fetch() + +Fetches config values from Firebase without activating them. Fetched values are not available until `activate()` is called. + +**Emits:** `fetch_completed` + +```gdscript +Firebase.remote_config.fetch() +``` + +--- + +{: .text-green-100 } +### activate() + +Activates the most recently fetched config values, making them available to the getters. + +**Emits:** `activate_completed` + +```gdscript +Firebase.remote_config.activate() +``` + +--- + +{: .text-green-100 } +### fetch_and_activate() + +Fetches and immediately activates config values in a single operation. + +**Emits:** `fetch_completed` + +```gdscript +Firebase.remote_config.fetch_and_activate() +``` + +--- + +## Typed Getters + +These methods return config values after they have been fetched and activated. If a key has not been fetched, the default value (from `set_defaults` or the type's zero value) is returned. + +{: .text-green-100 } +### get_string(key: String) -> String + +Returns a config value as a string. + +```gdscript +var message = Firebase.remote_config.get_string("welcome_message") +``` + +--- + +{: .text-green-100 } +### get_bool(key: String) -> bool + +Returns a config value as a boolean. + +```gdscript +var enabled = Firebase.remote_config.get_bool("feature_enabled") +``` + +--- + +{: .text-green-100 } +### get_int(key: String) -> int + +Returns a config value as an integer. + +```gdscript +var retries = Firebase.remote_config.get_int("max_retries") +``` + +--- + +{: .text-green-100 } +### get_float(key: String) -> float + +Returns a config value as a float. + +```gdscript +var multiplier = Firebase.remote_config.get_float("score_multiplier") +``` + +--- + +{: .text-green-100 } +### get_json(key: String) -> String + +Returns a config value as a raw JSON string. Use `JSON.parse_string()` to parse it. + +```gdscript +var json_str = Firebase.remote_config.get_json("level_config") +var data = JSON.parse_string(json_str) +``` + +--- + +{: .text-green-100 } +### get_all() -> Dictionary + +Returns all config values as a dictionary of strings (keys to string values). + +```gdscript +var all_config = Firebase.remote_config.get_all() +for key in all_config: + print("%s = %s" % [key, all_config[key]]) +``` + +--- + +## Status & Metadata + +{: .text-green-100 } +### get_value_source(key: String) -> int + +Returns the source of a config value. Useful for debugging whether a value comes from the server, defaults, or is static. + +| Value | Constant | Meaning | +|---|---|---| +| `0` | Static | No value set anywhere | +| `1` | Default | Using the default value from `set_defaults` | +| `2` | Remote | Using a value fetched from Firebase | + +```gdscript +var source = Firebase.remote_config.get_value_source("welcome_message") +``` + +--- + +{: .text-green-100 } +### get_last_fetch_status() -> int + +Returns the status of the last fetch attempt. + +| Value | Meaning | +|---|---| +| `-1` | Success | +| `0` | No fetch yet | +| `1` | Failure | +| `2` | Throttled | + +```gdscript +var status = Firebase.remote_config.get_last_fetch_status() +``` + +--- + +{: .text-green-100 } +### get_last_fetch_time() -> String + +Returns the time of the last successful fetch as an ISO 8601 string (e.g., `"2025-01-15T10:30:00Z"`). Returns an empty string if no fetch has occurred. + +```gdscript +var time = Firebase.remote_config.get_last_fetch_time() +``` + +--- + +## Real-Time Updates + +{: .text-green-100 } +### listen_for_updates() + +Starts listening for real-time config updates from Firebase. When config values are changed in the Firebase console, the `config_updated` signal fires with the list of changed keys. + +**Emits:** `config_updated` when remote values change. + +{: .note } +After receiving a `config_updated` signal, you still need to call `activate()` to apply the new values. + +```gdscript +Firebase.remote_config.listen_for_updates() + +# In your signal handler: +func _on_config_updated(updated_keys: Array): + print("Keys changed: ", updated_keys) + Firebase.remote_config.activate() +``` + +--- + +{: .text-green-100 } +### stop_listening_for_updates() + +Stops listening for real-time config updates. + +```gdscript +Firebase.remote_config.stop_listening_for_updates() +``` diff --git a/firebase/build.gradle.kts b/firebase/build.gradle.kts index 896ccdb..2ac8746 100644 --- a/firebase/build.gradle.kts +++ b/firebase/build.gradle.kts @@ -36,12 +36,14 @@ android { } dependencies { - implementation("org.godotengine:godot:4.4.1.stable") + implementation("org.godotengine:godot:4.6.1.stable") implementation("com.google.firebase:firebase-auth:23.2.0") implementation("com.google.android.gms:play-services-auth:21.3.0") implementation("com.google.firebase:firebase-firestore:25.1.4") implementation("com.google.firebase:firebase-database:21.0.0") implementation("com.google.firebase:firebase-storage:21.0.1") + implementation("com.google.firebase:firebase-analytics:22.4.0") + implementation("com.google.firebase:firebase-config:22.0.1") } // BUILD TASKS DEFINITION diff --git a/firebase/export_scripts_template/Firebase.gd b/firebase/export_scripts_template/Firebase.gd index bfedd23..cc626a0 100644 --- a/firebase/export_scripts_template/Firebase.gd +++ b/firebase/export_scripts_template/Firebase.gd @@ -4,6 +4,8 @@ var auth = preload("res://addons/GodotFirebaseAndroid/modules/Auth.gd").new() var firestore = preload("res://addons/GodotFirebaseAndroid/modules/Firestore.gd").new() var realtimeDB = preload("res://addons/GodotFirebaseAndroid/modules/RealtimeDB.gd").new() var storage = preload("res://addons/GodotFirebaseAndroid/modules/Storage.gd").new() +var analytics = preload("res://addons/GodotFirebaseAndroid/modules/Analytics.gd").new() +var remote_config = preload("res://addons/GodotFirebaseAndroid/modules/RemoteConfig.gd").new() func _ready() -> void: if Engine.has_singleton("GodotFirebaseAndroid"): @@ -20,6 +22,12 @@ func _ready() -> void: storage._plugin_singleton = _plugin_singleton storage._connect_signals() + + analytics._plugin_singleton = _plugin_singleton + analytics._connect_signals() + + remote_config._plugin_singleton = _plugin_singleton + remote_config._connect_signals() else: if not OS.has_feature("editor"): printerr("GodotFirebaseAndroid singleton not found!") diff --git a/firebase/export_scripts_template/export_plugin.gd b/firebase/export_scripts_template/export_plugin.gd index 49dc0b1..3c309a6 100644 --- a/firebase/export_scripts_template/export_plugin.gd +++ b/firebase/export_scripts_template/export_plugin.gd @@ -12,7 +12,9 @@ const DEPENDENCIES := [ "com.google.android.gms:play-services-auth:21.3.0", "com.google.firebase:firebase-firestore:25.1.4", "com.google.firebase:firebase-database:21.0.0", - "com.google.firebase:firebase-storage:21.0.1" + "com.google.firebase:firebase-storage:21.0.1", + "com.google.firebase:firebase-analytics:22.4.0", + "com.google.firebase:firebase-config:22.0.1" ] func _enter_tree(): diff --git a/firebase/export_scripts_template/modules/Analytics.gd b/firebase/export_scripts_template/modules/Analytics.gd new file mode 100644 index 0000000..a95c194 --- /dev/null +++ b/firebase/export_scripts_template/modules/Analytics.gd @@ -0,0 +1,46 @@ +extends Node + +signal app_instance_id_result(id: String) + +var _plugin_singleton: Object + +func _connect_signals(): + if not _plugin_singleton: + return + _plugin_singleton.connect("analytics_app_instance_id_result", app_instance_id_result.emit) + +func log_event(name: String, parameters: Dictionary) -> void: + if _plugin_singleton: + _plugin_singleton.analyticsLogEvent(name, parameters) + +func set_user_property(name: String, value: String) -> void: + if _plugin_singleton: + _plugin_singleton.analyticsSetUserProperty(name, value) + +func set_user_id(id: String) -> void: + if _plugin_singleton: + _plugin_singleton.analyticsSetUserId(id) + +func set_analytics_collection_enabled(enabled: bool) -> void: + if _plugin_singleton: + _plugin_singleton.analyticsSetAnalyticsCollectionEnabled(enabled) + +func reset_analytics_data() -> void: + if _plugin_singleton: + _plugin_singleton.analyticsResetAnalyticsData() + +func set_default_event_parameters(parameters: Dictionary) -> void: + if _plugin_singleton: + _plugin_singleton.analyticsSetDefaultEventParameters(parameters) + +func get_app_instance_id() -> void: + if _plugin_singleton: + _plugin_singleton.analyticsGetAppInstanceId() + +func set_consent(ad_storage: bool, analytics_storage: bool, ad_user_data: bool, ad_personalization: bool) -> void: + if _plugin_singleton: + _plugin_singleton.analyticsSetConsent(ad_storage, analytics_storage, ad_user_data, ad_personalization) + +func set_session_timeout(seconds: int) -> void: + if _plugin_singleton: + _plugin_singleton.analyticsSetSessionTimeout(seconds) diff --git a/firebase/export_scripts_template/modules/Auth.gd b/firebase/export_scripts_template/modules/Auth.gd index 2993bfb..689d92d 100644 --- a/firebase/export_scripts_template/modules/Auth.gd +++ b/firebase/export_scripts_template/modules/Auth.gd @@ -8,6 +8,11 @@ signal sign_out_success(success: bool) signal password_reset_sent(success: bool) signal email_verification_sent(success: bool) signal user_deleted(success: bool) +signal auth_state_changed(signed_in: bool, current_user_data: Dictionary) +signal id_token_result(token: String) +signal id_token_error(error_message: String) +signal profile_updated(success: bool) +signal profile_update_failure(error_message: String) var _plugin_singleton: Object @@ -22,6 +27,11 @@ func _connect_signals(): _plugin_singleton.connect("password_reset_sent", password_reset_sent.emit) _plugin_singleton.connect("email_verification_sent", email_verification_sent.emit) _plugin_singleton.connect("user_deleted", user_deleted.emit) + _plugin_singleton.connect("auth_state_changed", auth_state_changed.emit) + _plugin_singleton.connect("id_token_result", id_token_result.emit) + _plugin_singleton.connect("id_token_error", id_token_error.emit) + _plugin_singleton.connect("profile_updated", profile_updated.emit) + _plugin_singleton.connect("profile_update_failure", profile_update_failure.emit) func sign_in_anonymously() -> void: if _plugin_singleton: @@ -70,3 +80,39 @@ func is_signed_in() -> bool: func sign_out() -> void: if _plugin_singleton: _plugin_singleton.signOut() + +func use_emulator(host: String, port: int) -> void: + if _plugin_singleton: + _plugin_singleton.useAuthEmulator(host, port) + +func reauthenticate_with_email(email: String, password: String) -> void: + if _plugin_singleton: + _plugin_singleton.reauthenticateWithEmail(email, password) + +func add_auth_state_listener() -> void: + if _plugin_singleton: + _plugin_singleton.addAuthStateListener() + +func remove_auth_state_listener() -> void: + if _plugin_singleton: + _plugin_singleton.removeAuthStateListener() + +func get_id_token(force_refresh: bool = false) -> void: + if _plugin_singleton: + _plugin_singleton.getIdToken(force_refresh) + +func update_profile(display_name: String, photo_url: String = "") -> void: + if _plugin_singleton: + _plugin_singleton.updateProfile(display_name, photo_url) + +func update_password(new_password: String) -> void: + if _plugin_singleton: + _plugin_singleton.updatePassword(new_password) + +func reload_user() -> void: + if _plugin_singleton: + _plugin_singleton.reloadUser() + +func unlink_provider(provider_id: String) -> void: + if _plugin_singleton: + _plugin_singleton.unlinkProvider(provider_id) diff --git a/firebase/export_scripts_template/modules/Firestore.gd b/firebase/export_scripts_template/modules/Firestore.gd index c4feb18..b65cda8 100644 --- a/firebase/export_scripts_template/modules/Firestore.gd +++ b/firebase/export_scripts_template/modules/Firestore.gd @@ -5,6 +5,10 @@ signal get_task_completed(result: Dictionary) signal update_task_completed(result: Dictionary) signal delete_task_completed(result: Dictionary) signal document_changed(document_path: String, data: Dictionary) +signal query_task_completed(result: Dictionary) +signal collection_changed(collection_path: String, documents: Array) +signal batch_task_completed(result: Dictionary) +signal transaction_task_completed(result: Dictionary) var _plugin_singleton: Object @@ -16,6 +20,10 @@ func _connect_signals(): _plugin_singleton.connect("firestore_update_task_completed", update_task_completed.emit) _plugin_singleton.connect("firestore_delete_task_completed", delete_task_completed.emit) _plugin_singleton.connect("firestore_document_changed", document_changed.emit) + _plugin_singleton.connect("firestore_query_task_completed", query_task_completed.emit) + _plugin_singleton.connect("firestore_collection_changed", collection_changed.emit) + _plugin_singleton.connect("firestore_batch_task_completed", batch_task_completed.emit) + _plugin_singleton.connect("firestore_transaction_task_completed", transaction_task_completed.emit) func add_document(collection: String, data: Dictionary) -> void: if _plugin_singleton: @@ -45,6 +53,72 @@ func listen_to_document(documentPath: String) -> void: if _plugin_singleton: _plugin_singleton.firestoreListenToDocument(documentPath) +func query_documents(collection: String, filters: Array = [], order_by: String = "", order_descending: bool = false, limit_count: int = 0) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreQueryDocuments(collection, filters, order_by, order_descending, limit_count) + func stop_listening_to_document(documentPath: String) -> void: if _plugin_singleton: _plugin_singleton.firestoreStopListeningToDocument(documentPath) + +func use_emulator(host: String, port: int) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreUseEmulator(host, port) + +func listen_to_collection(collection: String) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreListenToCollection(collection) + +func stop_listening_to_collection(collection: String) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreStopListeningToCollection(collection) + +func create_batch() -> int: + if _plugin_singleton: + return _plugin_singleton.firestoreCreateBatch() + return -1 + +func batch_set(batch_id: int, collection: String, document_id: String, data: Dictionary, merge: bool = false) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreBatchSet(batch_id, collection, document_id, data, merge) + +func batch_update(batch_id: int, collection: String, document_id: String, data: Dictionary) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreBatchUpdate(batch_id, collection, document_id, data) + +func batch_delete(batch_id: int, collection: String, document_id: String) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreBatchDelete(batch_id, collection, document_id) + +func commit_batch(batch_id: int) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreCommitBatch(batch_id) + +func run_transaction(collection: String, document_id: String, update_data: Dictionary) -> void: + if _plugin_singleton: + _plugin_singleton.firestoreRunTransaction(collection, document_id, update_data) + +func server_timestamp() -> Dictionary: + if _plugin_singleton: + return _plugin_singleton.firestoreServerTimestamp() + return {} + +func array_union(elements: Array) -> Dictionary: + if _plugin_singleton: + return _plugin_singleton.firestoreArrayUnion(elements) + return {} + +func array_remove(elements: Array) -> Dictionary: + if _plugin_singleton: + return _plugin_singleton.firestoreArrayRemove(elements) + return {} + +func increment_by(value: float) -> Dictionary: + if _plugin_singleton: + return _plugin_singleton.firestoreIncrementBy(value) + return {} + +func delete_field() -> Dictionary: + if _plugin_singleton: + return _plugin_singleton.firestoreDeleteField() + return {} diff --git a/firebase/export_scripts_template/modules/RemoteConfig.gd b/firebase/export_scripts_template/modules/RemoteConfig.gd new file mode 100644 index 0000000..7d4dc68 --- /dev/null +++ b/firebase/export_scripts_template/modules/RemoteConfig.gd @@ -0,0 +1,104 @@ +extends Node + +signal fetch_completed(result: Dictionary) +signal activate_completed(result: Dictionary) +signal config_updated(updated_keys: Array) + +var _plugin_singleton: Object + +func _connect_signals(): + if not _plugin_singleton: + return + _plugin_singleton.connect("remote_config_fetch_completed", fetch_completed.emit) + _plugin_singleton.connect("remote_config_activate_completed", activate_completed.emit) + _plugin_singleton.connect("remote_config_updated", config_updated.emit) + +func initialize() -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigInitialize() + +func set_defaults(defaults: Dictionary) -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigSetDefaults(defaults) + +func set_minimum_fetch_interval(seconds: int) -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigSetMinimumFetchInterval(seconds) + +func set_fetch_timeout(seconds: int) -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigSetFetchTimeout(seconds) + +func fetch() -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigFetch() + +func activate() -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigActivate() + +func fetch_and_activate() -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigFetchAndActivate() + +func get_string(key: String) -> String: + var value := "" + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetString(key) + return value + +func get_bool(key: String) -> bool: + var value := false + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetBoolean(key) + return value + +func get_int(key: String) -> int: + var value := 0 + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetLong(key) + return value + +func get_float(key: String) -> float: + var value := 0.0 + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetDouble(key) + return value + +func get_all() -> Dictionary: + var value := {} + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetAll() + return value + +func get_json(key: String) -> String: + var value := "" + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetJson(key) + return value + +func get_value_source(key: String) -> int: + var value := 0 + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetValueSource(key) + return value + +func get_last_fetch_status() -> int: + var value := 0 + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetLastFetchStatus() + return value + +func get_last_fetch_time() -> String: + var value := "" + if _plugin_singleton: + value = _plugin_singleton.remoteConfigGetLastFetchTime() + return value + +func listen_for_updates() -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigListenForUpdates() + +func stop_listening_for_updates() -> void: + if _plugin_singleton: + _plugin_singleton.remoteConfigStopListeningForUpdates() diff --git a/firebase/src/main/java/org/godotengine/plugin/firebase/Analytics.kt b/firebase/src/main/java/org/godotengine/plugin/firebase/Analytics.kt new file mode 100644 index 0000000..fdb90cd --- /dev/null +++ b/firebase/src/main/java/org/godotengine/plugin/firebase/Analytics.kt @@ -0,0 +1,110 @@ +package org.godotengine.plugin.firebase + +import android.os.Bundle +import android.util.Log +import com.google.firebase.analytics.FirebaseAnalytics +import org.godotengine.godot.Dictionary +import org.godotengine.godot.plugin.SignalInfo + +class Analytics(private val plugin: FirebasePlugin) { + companion object { + private const val TAG = "GodotFirebaseAnalytics" + } + + private lateinit var firebaseAnalytics: FirebaseAnalytics + + fun init(activity: android.app.Activity) { + firebaseAnalytics = FirebaseAnalytics.getInstance(activity) + Log.d(TAG, "Firebase Analytics initialized") + } + + fun analyticsSignals(): MutableSet { + val signals: MutableSet = mutableSetOf() + signals.add(SignalInfo("analytics_app_instance_id_result", String::class.java)) + return signals + } + + fun logEvent(name: String, parameters: Dictionary) { + val bundle = Bundle() + for (key in parameters.keys) { + val value = parameters[key] + when (value) { + is String -> bundle.putString(key.toString(), value) + is Int -> bundle.putLong(key.toString(), value.toLong()) + is Long -> bundle.putLong(key.toString(), value) + is Double -> bundle.putDouble(key.toString(), value) + is Float -> bundle.putDouble(key.toString(), value.toDouble()) + is Boolean -> bundle.putLong(key.toString(), if (value) 1L else 0L) + else -> bundle.putString(key.toString(), value.toString()) + } + } + firebaseAnalytics.logEvent(name, bundle) + Log.d(TAG, "Event logged: $name") + } + + fun setUserProperty(name: String, value: String) { + firebaseAnalytics.setUserProperty(name, value) + Log.d(TAG, "User property set: $name = $value") + } + + fun setUserId(id: String) { + firebaseAnalytics.setUserId(id) + Log.d(TAG, "User ID set: $id") + } + + fun setAnalyticsCollectionEnabled(enabled: Boolean) { + firebaseAnalytics.setAnalyticsCollectionEnabled(enabled) + Log.d(TAG, "Analytics collection enabled: $enabled") + } + + fun resetAnalyticsData() { + firebaseAnalytics.resetAnalyticsData() + Log.d(TAG, "Analytics data reset") + } + + fun setDefaultEventParameters(parameters: Dictionary) { + val bundle = Bundle() + for (key in parameters.keys) { + val value = parameters[key] + when (value) { + is String -> bundle.putString(key.toString(), value) + is Int -> bundle.putLong(key.toString(), value.toLong()) + is Long -> bundle.putLong(key.toString(), value) + is Double -> bundle.putDouble(key.toString(), value) + is Float -> bundle.putDouble(key.toString(), value.toDouble()) + is Boolean -> bundle.putLong(key.toString(), if (value) 1L else 0L) + else -> bundle.putString(key.toString(), value.toString()) + } + } + firebaseAnalytics.setDefaultEventParameters(bundle) + Log.d(TAG, "Default event parameters set") + } + + fun getAppInstanceId() { + firebaseAnalytics.appInstanceId + .addOnSuccessListener { id -> + Log.d(TAG, "App instance ID: $id") + plugin.emitGodotSignal("analytics_app_instance_id_result", id ?: "") + } + .addOnFailureListener { e -> + Log.e(TAG, "Failed to get app instance ID", e) + plugin.emitGodotSignal("analytics_app_instance_id_result", "") + } + } + + fun setConsent(adStorage: Boolean, analyticsStorage: Boolean, adUserData: Boolean, adPersonalization: Boolean) { + val consentMap = mapOf( + FirebaseAnalytics.ConsentType.AD_STORAGE to if (adStorage) FirebaseAnalytics.ConsentStatus.GRANTED else FirebaseAnalytics.ConsentStatus.DENIED, + FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE to if (analyticsStorage) FirebaseAnalytics.ConsentStatus.GRANTED else FirebaseAnalytics.ConsentStatus.DENIED, + FirebaseAnalytics.ConsentType.AD_USER_DATA to if (adUserData) FirebaseAnalytics.ConsentStatus.GRANTED else FirebaseAnalytics.ConsentStatus.DENIED, + FirebaseAnalytics.ConsentType.AD_PERSONALIZATION to if (adPersonalization) FirebaseAnalytics.ConsentStatus.GRANTED else FirebaseAnalytics.ConsentStatus.DENIED + ) + firebaseAnalytics.setConsent(consentMap) + Log.d(TAG, "Consent set: adStorage=$adStorage, analyticsStorage=$analyticsStorage, adUserData=$adUserData, adPersonalization=$adPersonalization") + } + + fun setSessionTimeout(seconds: Int) { + firebaseAnalytics.setSessionTimeoutDuration(seconds * 1000L) + Log.d(TAG, "Session timeout set to $seconds seconds") + } +} diff --git a/firebase/src/main/java/org/godotengine/plugin/firebase/Authentication.kt b/firebase/src/main/java/org/godotengine/plugin/firebase/Authentication.kt index 73681f8..9165a76 100644 --- a/firebase/src/main/java/org/godotengine/plugin/firebase/Authentication.kt +++ b/firebase/src/main/java/org/godotengine/plugin/firebase/Authentication.kt @@ -2,13 +2,16 @@ package org.godotengine.plugin.firebase import android.app.Activity import android.content.Intent +import android.net.Uri import android.util.Log import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.common.api.ApiException +import com.google.firebase.auth.EmailAuthProvider import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.UserProfileChangeRequest import com.google.firebase.auth.auth import com.google.firebase.Firebase @@ -25,6 +28,7 @@ class Authentication(private val plugin: FirebasePlugin) { private val auth: FirebaseAuth = Firebase.auth private lateinit var googleSignInClient: GoogleSignInClient private var isLinkingAnonymous = false + private var authStateListener: FirebaseAuth.AuthStateListener? = null fun authSignals(): MutableSet { val signals: MutableSet = mutableSetOf() @@ -36,6 +40,11 @@ class Authentication(private val plugin: FirebasePlugin) { signals.add(SignalInfo("password_reset_sent", Boolean::class.javaObjectType)) signals.add(SignalInfo("email_verification_sent", Boolean::class.javaObjectType)) signals.add(SignalInfo("user_deleted", Boolean::class.javaObjectType)) + signals.add(SignalInfo("auth_state_changed", Boolean::class.javaObjectType, Dictionary::class.java)) + signals.add(SignalInfo("id_token_result", String::class.java)) + signals.add(SignalInfo("id_token_error", String::class.java)) + signals.add(SignalInfo("profile_updated", Boolean::class.javaObjectType)) + signals.add(SignalInfo("profile_update_failure", String::class.java)) return signals } @@ -265,4 +274,136 @@ class Authentication(private val plugin: FirebasePlugin) { } } + fun useEmulator(host: String, port: Int) { + auth.useEmulator(host, port) + Log.d(TAG, "Using Auth emulator at $host:$port") + } + + fun reauthenticateWithEmail(email: String, password: String) { + val user = auth.currentUser + if (user == null) { + plugin.emitGodotSignal("auth_failure", "No user signed in.") + return + } + val credential = EmailAuthProvider.getCredential(email, password) + user.reauthenticate(credential) + .addOnSuccessListener { + Log.d(TAG, "Reauthentication successful.") + plugin.emitGodotSignal("auth_success", getCurrentUser()) + } + .addOnFailureListener { e -> + Log.e(TAG, "Reauthentication failed", e) + plugin.emitGodotSignal("auth_failure", e.message ?: "Reauthentication failed") + } + } + + fun addAuthStateListener() { + if (authStateListener != null) return + authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth -> + val user = firebaseAuth.currentUser + val signedIn = user != null + plugin.emitGodotSignal("auth_state_changed", signedIn, getCurrentUser()) + } + auth.addAuthStateListener(authStateListener!!) + Log.d(TAG, "Auth state listener added.") + } + + fun removeAuthStateListener() { + authStateListener?.let { + auth.removeAuthStateListener(it) + authStateListener = null + Log.d(TAG, "Auth state listener removed.") + } + } + + fun getIdToken(forceRefresh: Boolean) { + val user = auth.currentUser + if (user == null) { + plugin.emitGodotSignal("id_token_error", "No user signed in.") + return + } + user.getIdToken(forceRefresh) + .addOnSuccessListener { result -> + val token = result.token ?: "" + Log.d(TAG, "ID token retrieved.") + plugin.emitGodotSignal("id_token_result", token) + } + .addOnFailureListener { e -> + Log.e(TAG, "Failed to get ID token", e) + plugin.emitGodotSignal("id_token_error", e.message ?: "Failed to get ID token") + } + } + + fun updateProfile(displayName: String, photoUrl: String) { + val user = auth.currentUser + if (user == null) { + plugin.emitGodotSignal("profile_update_failure", "No user signed in.") + return + } + val profileUpdates = UserProfileChangeRequest.Builder() + .setDisplayName(displayName.ifEmpty { null }) + .setPhotoUri(if (photoUrl.isNotEmpty()) Uri.parse(photoUrl) else null) + .build() + user.updateProfile(profileUpdates) + .addOnSuccessListener { + Log.d(TAG, "Profile updated.") + plugin.emitGodotSignal("profile_updated", true) + } + .addOnFailureListener { e -> + Log.e(TAG, "Profile update failed", e) + plugin.emitGodotSignal("profile_update_failure", e.message ?: "Profile update failed") + } + } + + fun updatePassword(newPassword: String) { + val user = auth.currentUser + if (user == null) { + plugin.emitGodotSignal("auth_failure", "No user signed in.") + return + } + user.updatePassword(newPassword) + .addOnSuccessListener { + Log.d(TAG, "Password updated.") + plugin.emitGodotSignal("auth_success", getCurrentUser()) + } + .addOnFailureListener { e -> + Log.e(TAG, "Password update failed", e) + plugin.emitGodotSignal("auth_failure", e.message ?: "Password update failed") + } + } + + fun reloadUser() { + val user = auth.currentUser + if (user == null) { + plugin.emitGodotSignal("auth_failure", "No user signed in.") + return + } + user.reload() + .addOnSuccessListener { + Log.d(TAG, "User reloaded.") + plugin.emitGodotSignal("auth_success", getCurrentUser()) + } + .addOnFailureListener { e -> + Log.e(TAG, "User reload failed", e) + plugin.emitGodotSignal("auth_failure", e.message ?: "User reload failed") + } + } + + fun unlinkProvider(providerId: String) { + val user = auth.currentUser + if (user == null) { + plugin.emitGodotSignal("auth_failure", "No user signed in.") + return + } + user.unlink(providerId) + .addOnSuccessListener { + Log.d(TAG, "Provider $providerId unlinked.") + plugin.emitGodotSignal("auth_success", getCurrentUser()) + } + .addOnFailureListener { e -> + Log.e(TAG, "Unlink provider failed", e) + plugin.emitGodotSignal("auth_failure", e.message ?: "Unlink failed") + } + } + } diff --git a/firebase/src/main/java/org/godotengine/plugin/firebase/FirebasePlugin.kt b/firebase/src/main/java/org/godotengine/plugin/firebase/FirebasePlugin.kt index 8e68a32..29c3a6f 100644 --- a/firebase/src/main/java/org/godotengine/plugin/firebase/FirebasePlugin.kt +++ b/firebase/src/main/java/org/godotengine/plugin/firebase/FirebasePlugin.kt @@ -16,9 +16,14 @@ class FirebasePlugin(godot: Godot) : GodotPlugin(godot) { private val firestore = Firestore(this) private val storage = CloudStorage(this) private val realtimeDatabase = RealtimeDatabase(this) + private val analytics = Analytics(this) + private val remoteConfig = RemoteConfig(this) override fun onMainCreate(activity: Activity?): View? { - activity?.let { auth.init(it) } + activity?.let { + auth.init(it) + analytics.init(it) + } return super.onMainCreate(activity) } @@ -32,6 +37,8 @@ class FirebasePlugin(godot: Godot) : GodotPlugin(godot) { signals.addAll(firestore.firestoreSignals()) signals.addAll(realtimeDatabase.realtimeDbSignals()) signals.addAll(storage.storageSignals()) + signals.addAll(analytics.analyticsSignals()) + signals.addAll(remoteConfig.remoteConfigSignals()) return signals } @@ -79,6 +86,33 @@ class FirebasePlugin(godot: Godot) : GodotPlugin(godot) { @UsedByGodot fun deleteUser() = auth.deleteUser() + @UsedByGodot + fun useAuthEmulator(host: String, port: Int) = auth.useEmulator(host, port) + + @UsedByGodot + fun reauthenticateWithEmail(email: String, password: String) = auth.reauthenticateWithEmail(email, password) + + @UsedByGodot + fun addAuthStateListener() = auth.addAuthStateListener() + + @UsedByGodot + fun removeAuthStateListener() = auth.removeAuthStateListener() + + @UsedByGodot + fun getIdToken(forceRefresh: Boolean) = auth.getIdToken(forceRefresh) + + @UsedByGodot + fun updateProfile(displayName: String, photoUrl: String) = auth.updateProfile(displayName, photoUrl) + + @UsedByGodot + fun updatePassword(newPassword: String) = auth.updatePassword(newPassword) + + @UsedByGodot + fun reloadUser() = auth.reloadUser() + + @UsedByGodot + fun unlinkProvider(providerId: String) = auth.unlinkProvider(providerId) + /** * Firestore */ @@ -101,12 +135,57 @@ class FirebasePlugin(godot: Godot) : GodotPlugin(godot) { @UsedByGodot fun firestoreGetDocumentsInCollection(collection: String) = firestore.getDocumentsInCollection(collection) + @UsedByGodot + fun firestoreQueryDocuments(collection: String, filters: Array, orderBy: String, orderDescending: Boolean, limitCount: Int) = firestore.queryDocuments(collection, filters, orderBy, orderDescending, limitCount) + @UsedByGodot fun firestoreListenToDocument(documentPath: String) = firestore.listenToDocument(documentPath) @UsedByGodot fun firestoreStopListeningToDocument(documentPath: String) = firestore.stopListeningToDocument(documentPath) + @UsedByGodot + fun firestoreUseEmulator(host: String, port: Int) = firestore.useEmulator(host, port) + + @UsedByGodot + fun firestoreListenToCollection(collection: String) = firestore.listenToCollection(collection) + + @UsedByGodot + fun firestoreStopListeningToCollection(collection: String) = firestore.stopListeningToCollection(collection) + + @UsedByGodot + fun firestoreCreateBatch() = firestore.createBatch() + + @UsedByGodot + fun firestoreBatchSet(batchId: Int, collection: String, documentId: String, data: Dictionary, merge: Boolean) = firestore.batchSet(batchId, collection, documentId, data, merge) + + @UsedByGodot + fun firestoreBatchUpdate(batchId: Int, collection: String, documentId: String, data: Dictionary) = firestore.batchUpdate(batchId, collection, documentId, data) + + @UsedByGodot + fun firestoreBatchDelete(batchId: Int, collection: String, documentId: String) = firestore.batchDelete(batchId, collection, documentId) + + @UsedByGodot + fun firestoreCommitBatch(batchId: Int) = firestore.commitBatch(batchId) + + @UsedByGodot + fun firestoreRunTransaction(collection: String, documentId: String, updateData: Dictionary) = firestore.runTransaction(collection, documentId, updateData) + + @UsedByGodot + fun firestoreServerTimestamp() = firestore.serverTimestamp() + + @UsedByGodot + fun firestoreArrayUnion(elements: Array) = firestore.arrayUnion(elements) + + @UsedByGodot + fun firestoreArrayRemove(elements: Array) = firestore.arrayRemove(elements) + + @UsedByGodot + fun firestoreIncrementBy(value: Double) = firestore.incrementBy(value) + + @UsedByGodot + fun firestoreDeleteField() = firestore.deleteField() + /** * Cloud Storage */ @@ -150,4 +229,93 @@ class FirebasePlugin(godot: Godot) : GodotPlugin(godot) { @UsedByGodot fun rtdbStopListening(path: String) = realtimeDatabase.stopListening(path) + + /** + * Analytics + */ + + @UsedByGodot + fun analyticsLogEvent(name: String, parameters: Dictionary) = analytics.logEvent(name, parameters) + + @UsedByGodot + fun analyticsSetUserProperty(name: String, value: String) = analytics.setUserProperty(name, value) + + @UsedByGodot + fun analyticsSetUserId(id: String) = analytics.setUserId(id) + + @UsedByGodot + fun analyticsSetAnalyticsCollectionEnabled(enabled: Boolean) = analytics.setAnalyticsCollectionEnabled(enabled) + + @UsedByGodot + fun analyticsResetAnalyticsData() = analytics.resetAnalyticsData() + + @UsedByGodot + fun analyticsSetDefaultEventParameters(parameters: Dictionary) = analytics.setDefaultEventParameters(parameters) + + @UsedByGodot + fun analyticsGetAppInstanceId() = analytics.getAppInstanceId() + + @UsedByGodot + fun analyticsSetConsent(adStorage: Boolean, analyticsStorage: Boolean, adUserData: Boolean, adPersonalization: Boolean) = analytics.setConsent(adStorage, analyticsStorage, adUserData, adPersonalization) + + @UsedByGodot + fun analyticsSetSessionTimeout(seconds: Int) = analytics.setSessionTimeout(seconds) + + /** + * Remote Config + */ + + @UsedByGodot + fun remoteConfigInitialize() = remoteConfig.initialize() + + @UsedByGodot + fun remoteConfigSetDefaults(defaults: Dictionary) = remoteConfig.setDefaults(defaults) + + @UsedByGodot + fun remoteConfigSetMinimumFetchInterval(seconds: Long) = remoteConfig.setMinimumFetchInterval(seconds) + + @UsedByGodot + fun remoteConfigSetFetchTimeout(seconds: Long) = remoteConfig.setFetchTimeout(seconds) + + @UsedByGodot + fun remoteConfigFetch() = remoteConfig.fetch() + + @UsedByGodot + fun remoteConfigActivate() = remoteConfig.activate() + + @UsedByGodot + fun remoteConfigFetchAndActivate() = remoteConfig.fetchAndActivate() + + @UsedByGodot + fun remoteConfigGetString(key: String) = remoteConfig.getString(key) + + @UsedByGodot + fun remoteConfigGetBoolean(key: String) = remoteConfig.getBoolean(key) + + @UsedByGodot + fun remoteConfigGetLong(key: String) = remoteConfig.getLong(key) + + @UsedByGodot + fun remoteConfigGetDouble(key: String) = remoteConfig.getDouble(key) + + @UsedByGodot + fun remoteConfigGetAll() = remoteConfig.getAll() + + @UsedByGodot + fun remoteConfigGetJson(key: String) = remoteConfig.getJson(key) + + @UsedByGodot + fun remoteConfigGetValueSource(key: String) = remoteConfig.getValueSource(key) + + @UsedByGodot + fun remoteConfigGetLastFetchStatus() = remoteConfig.getLastFetchStatus() + + @UsedByGodot + fun remoteConfigGetLastFetchTime() = remoteConfig.getLastFetchTime() + + @UsedByGodot + fun remoteConfigListenForUpdates() = remoteConfig.listenForUpdates() + + @UsedByGodot + fun remoteConfigStopListeningForUpdates() = remoteConfig.stopListeningForUpdates() } diff --git a/firebase/src/main/java/org/godotengine/plugin/firebase/Firestore.kt b/firebase/src/main/java/org/godotengine/plugin/firebase/Firestore.kt index af0a7ad..9759228 100644 --- a/firebase/src/main/java/org/godotengine/plugin/firebase/Firestore.kt +++ b/firebase/src/main/java/org/godotengine/plugin/firebase/Firestore.kt @@ -3,8 +3,11 @@ package org.godotengine.plugin.firebase import android.util.Log import com.google.firebase.Firebase import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.ListenerRegistration +import com.google.firebase.firestore.Query import com.google.firebase.firestore.SetOptions +import com.google.firebase.firestore.WriteBatch import com.google.firebase.firestore.firestore import org.godotengine.godot.Dictionary import org.godotengine.godot.plugin.SignalInfo @@ -16,6 +19,9 @@ class Firestore(private val plugin: FirebasePlugin) { private val firestore = Firebase.firestore private val documentListeners: MutableMap = mutableMapOf() + private val collectionListeners: MutableMap = mutableMapOf() + private val batches: MutableMap = mutableMapOf() + private var batchCounter = 0 fun firestoreSignals(): MutableSet { val signals: MutableSet = mutableSetOf() @@ -24,6 +30,10 @@ class Firestore(private val plugin: FirebasePlugin) { signals.add(SignalInfo("firestore_update_task_completed", Dictionary::class.java)) signals.add(SignalInfo("firestore_delete_task_completed", Dictionary::class.java)) signals.add(SignalInfo("firestore_document_changed", String::class.java, Dictionary::class.java)) + signals.add(SignalInfo("firestore_query_task_completed", Dictionary::class.java)) + signals.add(SignalInfo("firestore_collection_changed", String::class.java, Array::class.java)) + signals.add(SignalInfo("firestore_batch_task_completed", Dictionary::class.java)) + signals.add(SignalInfo("firestore_transaction_task_completed", Dictionary::class.java)) return signals } @@ -41,8 +51,13 @@ class Firestore(private val plugin: FirebasePlugin) { return result } + fun useEmulator(host: String, port: Int) { + firestore.useEmulator(host, port) + Log.d(TAG, "Using Firestore emulator at $host:$port") + } + fun addDocument(collection: String, data: Dictionary) { - val map = data.toMap() + val map = convertDataForFirestore(data.toMap()) firestore.collection(collection).add(map) .addOnSuccessListener { documentRef -> @@ -57,7 +72,7 @@ class Firestore(private val plugin: FirebasePlugin) { } fun setDocument(collection: String, documentId: String, data: Dictionary, merge: Boolean = false) { - val map = data.toMap() + val map = convertDataForFirestore(data.toMap()) val docRef = firestore.collection(collection).document(documentId) val task = if (merge) docRef.set(map, SetOptions.merge()) else docRef.set(map) @@ -107,7 +122,7 @@ class Firestore(private val plugin: FirebasePlugin) { } fun updateDocument(collection: String, documentId: String, data: Dictionary) { - val map = data.toMap() + val map = convertDataForFirestore(data.toMap()) firestore.collection(collection).document(documentId).update(map) .addOnSuccessListener { Log.d(TAG, "Document $documentId updated successfully") @@ -154,6 +169,273 @@ class Firestore(private val plugin: FirebasePlugin) { } + fun queryDocuments( + collection: String, + filters: Array, + orderBy: String, + orderDescending: Boolean, + limitCount: Int + ) { + var query: Query = firestore.collection(collection) + + for (item in filters) { + if (item is Dictionary) { + val field = item["field"] as? String ?: continue + val op = item["op"] as? String ?: continue + val value = item["value"] ?: continue + + query = when (op) { + "==" -> query.whereEqualTo(field, value) + "!=" -> query.whereNotEqualTo(field, value) + "<" -> query.whereLessThan(field, value) + "<=" -> query.whereLessThanOrEqualTo(field, value) + ">" -> query.whereGreaterThan(field, value) + ">=" -> query.whereGreaterThanOrEqualTo(field, value) + "array_contains" -> query.whereArrayContains(field, value) + "in" -> { + val list = when (value) { + is Array<*> -> value.toList() + is List<*> -> value + else -> listOf(value) + } + query.whereIn(field, list) + } + "not_in" -> { + val list = when (value) { + is Array<*> -> value.toList() + is List<*> -> value + else -> listOf(value) + } + query.whereNotIn(field, list) + } + "array_contains_any" -> { + val list = when (value) { + is Array<*> -> value.toList() + is List<*> -> value + else -> listOf(value) + } + query.whereArrayContainsAny(field, list) + } + else -> { + Log.w(TAG, "Unknown filter operator: $op") + query + } + } + } + } + + if (orderBy.isNotEmpty()) { + query = query.orderBy( + orderBy, + if (orderDescending) Query.Direction.DESCENDING else Query.Direction.ASCENDING + ) + } + + if (limitCount > 0) { + query = query.limit(limitCount.toLong()) + } + + query.get() + .addOnSuccessListener { querySnapshot -> + val documents = mutableListOf() + for (doc in querySnapshot.documents) { + val docDict = Dictionary() + docDict["docID"] = doc.id + docDict["data"] = snapshotToDictionary(doc) + documents.add(docDict) + } + val result = Dictionary() + result["status"] = true + result["documents"] = documents.toTypedArray() + Log.d(TAG, "Query completed successfully, ${documents.size} documents found") + plugin.emitGodotSignal("firestore_query_task_completed", result) + } + .addOnFailureListener { e -> + Log.e(TAG, "Error querying documents:", e) + val result = Dictionary() + result["status"] = false + result["error"] = e.message ?: "Unknown error" + plugin.emitGodotSignal("firestore_query_task_completed", result) + } + } + + fun listenToCollection(collection: String) { + collectionListeners[collection]?.remove() + val colRef = firestore.collection(collection) + val listener = colRef.addSnapshotListener { snapshot, error -> + if (error != null) { + Log.e(TAG, "Listen failed for collection $collection", error) + return@addSnapshotListener + } + if (snapshot != null) { + val documents = mutableListOf() + for (doc in snapshot.documents) { + val docDict = Dictionary() + docDict["docID"] = doc.id + docDict["data"] = snapshotToDictionary(doc) + documents.add(docDict) + } + Log.d(TAG, "Collection changed at $collection") + plugin.emitGodotSignal("firestore_collection_changed", collection, documents.toTypedArray()) + } + } + collectionListeners[collection] = listener + } + + fun stopListeningToCollection(collection: String) { + collectionListeners[collection]?.remove() + collectionListeners.remove(collection) + Log.d(TAG, "Stopped listening to collection $collection") + } + + fun createBatch(): Int { + batchCounter++ + batches[batchCounter] = firestore.batch() + Log.d(TAG, "Created batch $batchCounter") + return batchCounter + } + + fun batchSet(batchId: Int, collection: String, documentId: String, data: Dictionary, merge: Boolean) { + val batch = batches[batchId] ?: run { + Log.e(TAG, "Batch $batchId not found") + return + } + val docRef = firestore.collection(collection).document(documentId) + val map = convertDataForFirestore(data.toMap()) + if (merge) batch.set(docRef, map, SetOptions.merge()) else batch.set(docRef, map) + } + + fun batchUpdate(batchId: Int, collection: String, documentId: String, data: Dictionary) { + val batch = batches[batchId] ?: run { + Log.e(TAG, "Batch $batchId not found") + return + } + val docRef = firestore.collection(collection).document(documentId) + batch.update(docRef, convertDataForFirestore(data.toMap())) + } + + fun batchDelete(batchId: Int, collection: String, documentId: String) { + val batch = batches[batchId] ?: run { + Log.e(TAG, "Batch $batchId not found") + return + } + val docRef = firestore.collection(collection).document(documentId) + batch.delete(docRef) + } + + fun commitBatch(batchId: Int) { + val batch = batches[batchId] ?: run { + Log.e(TAG, "Batch $batchId not found") + plugin.emitGodotSignal("firestore_batch_task_completed", createResultDict(false, error = "Batch $batchId not found")) + return + } + batch.commit() + .addOnSuccessListener { + batches.remove(batchId) + Log.d(TAG, "Batch $batchId committed successfully") + plugin.emitGodotSignal("firestore_batch_task_completed", createResultDict(true)) + } + .addOnFailureListener { e -> + batches.remove(batchId) + Log.e(TAG, "Batch $batchId commit failed", e) + plugin.emitGodotSignal("firestore_batch_task_completed", createResultDict(false, error = e.message)) + } + } + + fun runTransaction(collection: String, documentId: String, updateData: Dictionary) { + val docRef = firestore.collection(collection).document(documentId) + firestore.runTransaction { transaction -> + transaction.get(docRef) + transaction.update(docRef, convertDataForFirestore(updateData.toMap())) + null + } + .addOnSuccessListener { + Log.d(TAG, "Transaction completed for $collection/$documentId") + plugin.emitGodotSignal("firestore_transaction_task_completed", createResultDict(true, documentId)) + } + .addOnFailureListener { e -> + Log.e(TAG, "Transaction failed for $collection/$documentId", e) + plugin.emitGodotSignal("firestore_transaction_task_completed", createResultDict(false, documentId, e.message)) + } + } + + fun serverTimestamp(): Dictionary { + val d = Dictionary() + d["__fieldValue"] = "serverTimestamp" + return d + } + + fun arrayUnion(elements: Array): Dictionary { + val d = Dictionary() + d["__fieldValue"] = "arrayUnion" + d["elements"] = elements + return d + } + + fun arrayRemove(elements: Array): Dictionary { + val d = Dictionary() + d["__fieldValue"] = "arrayRemove" + d["elements"] = elements + return d + } + + fun incrementBy(value: Double): Dictionary { + val d = Dictionary() + d["__fieldValue"] = "increment" + d["value"] = value + return d + } + + fun deleteField(): Dictionary { + val d = Dictionary() + d["__fieldValue"] = "deleteField" + return d + } + + private fun convertDataForFirestore(map: Map<*, *>): Map { + val result = mutableMapOf() + map.forEach { (k, v) -> + val key = k?.toString() ?: return@forEach + val converted = convertValueForFirestore(v) + if (converted != null) result[key] = converted + } + return result + } + + private fun convertValueForFirestore(value: Any?): Any? { + return when (value) { + null -> null + is Dictionary -> { + val fieldValueType = value["__fieldValue"] as? String + when (fieldValueType) { + "serverTimestamp" -> FieldValue.serverTimestamp() + "arrayUnion" -> { + val elements = (value["elements"] as? Array<*>)?.filterNotNull()?.toTypedArray() ?: emptyArray() + FieldValue.arrayUnion(*elements) + } + "arrayRemove" -> { + val elements = (value["elements"] as? Array<*>)?.filterNotNull()?.toTypedArray() ?: emptyArray() + FieldValue.arrayRemove(*elements) + } + "increment" -> { + when (val v = value["value"]) { + is Long -> FieldValue.increment(v) + is Double -> FieldValue.increment(v) + is Int -> FieldValue.increment(v.toLong()) + else -> FieldValue.increment(0L) + } + } + "deleteField" -> FieldValue.delete() + else -> convertDataForFirestore(value.toMap()) + } + } + is Map<*, *> -> convertDataForFirestore(value) + is List<*> -> value.map { convertValueForFirestore(it) }.toTypedArray() + is Array<*> -> value.map { convertValueForFirestore(it) }.toTypedArray() + else -> value + } + } + private fun snapshotToDictionary(snapshot: DocumentSnapshot): Dictionary { val dict = Dictionary() if (snapshot.exists()) { diff --git a/firebase/src/main/java/org/godotengine/plugin/firebase/RemoteConfig.kt b/firebase/src/main/java/org/godotengine/plugin/firebase/RemoteConfig.kt new file mode 100644 index 0000000..892a84b --- /dev/null +++ b/firebase/src/main/java/org/godotengine/plugin/firebase/RemoteConfig.kt @@ -0,0 +1,195 @@ +package org.godotengine.plugin.firebase + +import android.util.Log +import com.google.firebase.remoteconfig.ConfigUpdate +import com.google.firebase.remoteconfig.ConfigUpdateListener +import com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.FirebaseRemoteConfigException +import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings +import org.godotengine.godot.Dictionary +import org.godotengine.godot.plugin.SignalInfo +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +class RemoteConfig(private val plugin: FirebasePlugin) { + companion object { + private const val TAG = "GodotFirebaseRemoteConfig" + } + + private val remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance() + private var minimumFetchIntervalSeconds: Long = 3600 + private var fetchTimeoutSeconds: Long = 60 + private var configUpdateListener: ConfigUpdateListenerRegistration? = null + + fun remoteConfigSignals(): MutableSet { + val signals: MutableSet = mutableSetOf() + signals.add(SignalInfo("remote_config_fetch_completed", Dictionary::class.java)) + signals.add(SignalInfo("remote_config_activate_completed", Dictionary::class.java)) + signals.add(SignalInfo("remote_config_updated", Array::class.java)) + return signals + } + + fun initialize() { + applyConfigSettings() + Log.d(TAG, "Remote Config initialized") + } + + fun setMinimumFetchInterval(seconds: Long) { + minimumFetchIntervalSeconds = seconds + applyConfigSettings() + Log.d(TAG, "Minimum fetch interval set to $seconds seconds") + } + + fun setFetchTimeout(seconds: Long) { + fetchTimeoutSeconds = seconds + applyConfigSettings() + Log.d(TAG, "Fetch timeout set to $seconds seconds") + } + + private fun applyConfigSettings() { + val configSettings = FirebaseRemoteConfigSettings.Builder() + .setMinimumFetchIntervalInSeconds(minimumFetchIntervalSeconds) + .setFetchTimeoutInSeconds(fetchTimeoutSeconds) + .build() + remoteConfig.setConfigSettingsAsync(configSettings) + } + + fun setDefaults(defaults: Dictionary) { + val defaultsMap = mutableMapOf() + for (key in defaults.keys) { + val value = defaults[key] + if (value != null) { + defaultsMap[key.toString()] = value + } + } + remoteConfig.setDefaultsAsync(defaultsMap) + Log.d(TAG, "Defaults set: ${defaultsMap.keys}") + } + + fun fetch() { + remoteConfig.fetch() + .addOnSuccessListener { + val result = Dictionary() + result["status"] = true + result["error"] = "" + Log.d(TAG, "Fetch succeeded") + plugin.emitGodotSignal("remote_config_fetch_completed", result) + } + .addOnFailureListener { e -> + val result = Dictionary() + result["status"] = false + result["error"] = e.message ?: "Unknown error" + Log.e(TAG, "Fetch failed", e) + plugin.emitGodotSignal("remote_config_fetch_completed", result) + } + } + + fun activate() { + remoteConfig.activate() + .addOnSuccessListener { activated -> + val result = Dictionary() + result["status"] = true + result["activated"] = activated + result["error"] = "" + Log.d(TAG, "Activate succeeded (activated=$activated)") + plugin.emitGodotSignal("remote_config_activate_completed", result) + } + .addOnFailureListener { e -> + val result = Dictionary() + result["status"] = false + result["activated"] = false + result["error"] = e.message ?: "Unknown error" + Log.e(TAG, "Activate failed", e) + plugin.emitGodotSignal("remote_config_activate_completed", result) + } + } + + fun fetchAndActivate() { + remoteConfig.fetchAndActivate() + .addOnSuccessListener { activated -> + val result = Dictionary() + result["status"] = true + result["activated"] = activated + result["error"] = "" + Log.d(TAG, "Fetch and activate succeeded (activated=$activated)") + plugin.emitGodotSignal("remote_config_fetch_completed", result) + } + .addOnFailureListener { e -> + val result = Dictionary() + result["status"] = false + result["activated"] = false + result["error"] = e.message ?: "Unknown error" + Log.e(TAG, "Fetch and activate failed", e) + plugin.emitGodotSignal("remote_config_fetch_completed", result) + } + } + + fun getString(key: String): String { + return remoteConfig.getString(key) + } + + fun getBoolean(key: String): Boolean { + return remoteConfig.getBoolean(key) + } + + fun getLong(key: String): Long { + return remoteConfig.getLong(key) + } + + fun getDouble(key: String): Double { + return remoteConfig.getDouble(key) + } + + fun getAll(): Dictionary { + val result = Dictionary() + for ((key, value) in remoteConfig.all) { + result[key] = value.asString() + } + return result + } + + fun getJson(key: String): String { + return remoteConfig.getString(key) + } + + fun getValueSource(key: String): Int { + return remoteConfig.getValue(key).source + } + + fun getLastFetchStatus(): Int { + return remoteConfig.info.lastFetchStatus + } + + fun getLastFetchTime(): String { + val millis = remoteConfig.info.fetchTimeMillis + if (millis <= 0) return "" + val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + sdf.timeZone = TimeZone.getTimeZone("UTC") + return sdf.format(Date(millis)) + } + + fun listenForUpdates() { + if (configUpdateListener != null) return + configUpdateListener = remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener { + override fun onUpdate(configUpdate: ConfigUpdate) { + val updatedKeys = configUpdate.updatedKeys.toTypedArray() + Log.d(TAG, "Config updated, keys: ${updatedKeys.toList()}") + plugin.emitGodotSignal("remote_config_updated", updatedKeys) + } + + override fun onError(error: FirebaseRemoteConfigException) { + Log.e(TAG, "Config update listener error", error) + } + }) + Log.d(TAG, "Config update listener added") + } + + fun stopListeningForUpdates() { + configUpdateListener?.remove() + configUpdateListener = null + Log.d(TAG, "Config update listener removed") + } +} diff --git a/tmp/firebase-emulator-testing-investigation.md b/tmp/firebase-emulator-testing-investigation.md new file mode 100644 index 0000000..95a9a05 --- /dev/null +++ b/tmp/firebase-emulator-testing-investigation.md @@ -0,0 +1,101 @@ +# Feasibility: Automated Testing with Firebase Emulator + gdUnit4 + +## Context + +The plugin is growing in scope (6 modules, ~70+ exposed methods). We need to evaluate whether automated testing via Firebase Emulator Suite + gdUnit4 is feasible, and if so, how to set it up in GitHub Actions CI. + +--- + +## Key Finding: The Core Constraint + +**gdUnit4 cannot test Kotlin Android plugin code.** + +gdUnit4 runs exclusively in the Godot editor / headless mode. Android plugins (.aar + Kotlin) are only loaded inside an exported Android APK — never in the editor. This means gdUnit4 is the wrong tool for the heaviest part of the plugin. + +--- + +## What Can Actually Be Tested + +### Layer 1 — Kotlin plugin code (the real logic) +**Tool:** JVM unit tests (JUnit 4/5 + Mockito) run via `./gradlew test` +**Firebase Emulator:** Runs on JVM — use `localhost:port`, no Android emulator required +**Coverage:** Auth, Firestore, Realtime DB, Storage, Remote Config logic +**CI complexity:** Low — standard `ubuntu-latest` runner, install Node.js + Firebase CLI + JDK +**Verdict:** ✅ Feasible and relatively easy + +### Layer 2 — GDScript wrapper code (export_scripts_template/) +**Tool:** gdUnit4 headless CLI (`runtest.sh`) +**Problem:** The wrappers call `Engine.get_singleton("GodotFirebaseAndroid")` which returns null outside Android. Tests would need heavy mocking of the plugin singleton interface. +**CI complexity:** Moderate — needs Godot binary in runner, wrapper logic is thin (mostly signal connections + delegation) +**Verdict:** ⚠️ Feasible but low ROI — wrappers are thin delegation layers, not complex logic + +### Layer 3 — End-to-end (GDScript → Kotlin → Firebase Emulator) +**Tool:** Android Espresso instrumented tests on Android emulator +**CI complexity:** High — needs `reactivecircle/android-emulator-runner@v2`, slow (~15–20 min per run) +**Verdict:** ⚠️ Feasible but heavyweight for CI + +--- + +## Recommended Approach (Phased) + +### Phase 1 — JVM Kotlin Tests + Firebase Emulator (highest value, lowest friction) + +Set up `./gradlew test` with Firebase Emulator: + +1. Add JUnit 4 test dependencies to `firebase/build.gradle.kts` +2. Create `firebase/src/test/java/.../firebase/` test classes per module +3. Configure Firebase Emulator for JVM: Auth (port 9099), Firestore (8080), Realtime DB (9000), Storage (9199) +4. Tests call Kotlin module methods directly, verify signal emissions via callbacks + +**CI workflow additions:** +```yaml +- uses: actions/setup-node@v4 + with: { node-version: '20' } +- run: npm install -g firebase-tools +- run: firebase emulators:start --only auth,firestore,database,storage & +- run: ./gradlew test +``` + +This gives direct coverage of the actual Firebase SDK integration — no mocking needed because the Emulator is a real Firebase server locally. + +### Phase 2 — gdUnit4 for GDScript (optional, if wrappers grow in complexity) + +Only worth adding if wrappers gain non-trivial logic (retry policies, caching, error normalization, etc.). Current wrappers are ~20 lines each — not much to test. + +### Phase 3 — Android Instrumented Tests (future, for end-to-end confidence) + +When the plugin is more mature, Espresso + `android-emulator-runner@v2` gives true end-to-end coverage. The plugin already has `use_emulator()` methods in Auth.gd and presumably Firestore, making this architecturally ready. + +--- + +## Existing Emulator Support in the Plugin + +The plugin already anticipates emulator use: +- `Auth.gd` exposes `use_emulator(host, port)` → calls `authentication_use_emulator` on Kotlin +- This means **the architecture is already prepared** for emulator-based testing + +--- + +## CI Feasibility Summary + +| Test Strategy | CI Difficulty | Setup Time | Coverage | +|---|---|---|---| +| JVM Kotlin + Firebase Emulator | Low | ~1 day | Auth, Firestore, DB, Storage, Config | +| gdUnit4 headless (GDScript) | Moderate | ~2 days | Wrapper delegation logic only | +| Android Espresso + Emulator | High | ~3–5 days | True end-to-end | + +**Short answer:** Yes, it's technically feasible. Phase 1 (JVM tests + Firebase Emulator) is the easiest, highest-value starting point and can run on standard GitHub Actions without an Android emulator. + +--- + +## Critical Files to Modify + +- `firebase/build.gradle.kts` — add JUnit test deps +- `firebase/src/test/java/org/godotengine/plugin/firebase/` — new test classes (to be created) +- `.github/workflows/` — update CI to install Firebase CLI and run emulator + +## Verification + +1. `./gradlew test` passes with emulator running locally +2. Firebase Emulator starts and stops cleanly in CI logs +3. Auth/Firestore/DB/Storage test cases emit expected signals