From b4639e80bcaae5ad9ec9b7b10bb2ed80c1600d8b Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:37:47 +0300 Subject: [PATCH] download system --- flumi/Assets/Icons/folder.svg | 1 + flumi/Assets/Icons/folder.svg.import | 37 +++ flumi/Assets/Icons/link-2.svg | 1 + flumi/Assets/Icons/link-2.svg.import | 37 +++ flumi/Scenes/BrowserMenus/download_entry.tscn | 106 +++++++++ flumi/Scenes/BrowserMenus/downloads.tscn | 88 ++++++++ flumi/Scenes/UI/DownloadDialog.tscn | 118 ++++++++++ flumi/Scenes/UI/DownloadProgress.tscn | 85 +++++++ flumi/Scripts/B9/Lua.gd | 8 + flumi/Scripts/Browser/DownloadDialog.gd | 98 ++++++++ flumi/Scripts/Browser/DownloadDialog.gd.uid | 1 + flumi/Scripts/Browser/DownloadEntry.gd | 70 ++++++ flumi/Scripts/Browser/DownloadEntry.gd.uid | 1 + flumi/Scripts/Browser/DownloadManager.gd | 211 ++++++++++++++++++ flumi/Scripts/Browser/DownloadManager.gd.uid | 1 + flumi/Scripts/Browser/DownloadProgress.gd | 129 +++++++++++ flumi/Scripts/Browser/DownloadProgress.gd.uid | 1 + flumi/Scripts/Browser/DownloadsStore.gd | 76 +++++++ flumi/Scripts/Browser/DownloadsStore.gd.uid | 1 + flumi/Scripts/Browser/OptionButton.gd | 9 + flumi/Scripts/Engine/FontManager.gd | 2 +- flumi/Scripts/Tags/audio.gd | 2 +- flumi/Scripts/Utils/Lua/Download.gd | 65 ++++++ flumi/Scripts/Utils/Lua/Download.gd.uid | 1 + flumi/Scripts/Utils/Lua/Event.gd | 2 + flumi/Scripts/Utils/Lua/ThreadedVM.gd | 1 + flumi/Scripts/main.gd | 6 + tests/download.html | 159 +++++++++++++ 28 files changed, 1315 insertions(+), 2 deletions(-) create mode 100644 flumi/Assets/Icons/folder.svg create mode 100644 flumi/Assets/Icons/folder.svg.import create mode 100644 flumi/Assets/Icons/link-2.svg create mode 100644 flumi/Assets/Icons/link-2.svg.import create mode 100644 flumi/Scenes/BrowserMenus/download_entry.tscn create mode 100644 flumi/Scenes/BrowserMenus/downloads.tscn create mode 100644 flumi/Scenes/UI/DownloadDialog.tscn create mode 100644 flumi/Scenes/UI/DownloadProgress.tscn create mode 100644 flumi/Scripts/Browser/DownloadDialog.gd create mode 100644 flumi/Scripts/Browser/DownloadDialog.gd.uid create mode 100644 flumi/Scripts/Browser/DownloadEntry.gd create mode 100644 flumi/Scripts/Browser/DownloadEntry.gd.uid create mode 100644 flumi/Scripts/Browser/DownloadManager.gd create mode 100644 flumi/Scripts/Browser/DownloadManager.gd.uid create mode 100644 flumi/Scripts/Browser/DownloadProgress.gd create mode 100644 flumi/Scripts/Browser/DownloadProgress.gd.uid create mode 100644 flumi/Scripts/Browser/DownloadsStore.gd create mode 100644 flumi/Scripts/Browser/DownloadsStore.gd.uid create mode 100644 flumi/Scripts/Utils/Lua/Download.gd create mode 100644 flumi/Scripts/Utils/Lua/Download.gd.uid create mode 100644 tests/download.html diff --git a/flumi/Assets/Icons/folder.svg b/flumi/Assets/Icons/folder.svg new file mode 100644 index 0000000..9d4905a --- /dev/null +++ b/flumi/Assets/Icons/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/folder.svg.import b/flumi/Assets/Icons/folder.svg.import new file mode 100644 index 0000000..c157ab3 --- /dev/null +++ b/flumi/Assets/Icons/folder.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3ottyh38w3db" +path="res://.godot/imported/folder.svg-0d6e9c106058d2e35908257faa439409.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/folder.svg" +dest_files=["res://.godot/imported/folder.svg-0d6e9c106058d2e35908257faa439409.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Assets/Icons/link-2.svg b/flumi/Assets/Icons/link-2.svg new file mode 100644 index 0000000..7cc8f0a --- /dev/null +++ b/flumi/Assets/Icons/link-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/link-2.svg.import b/flumi/Assets/Icons/link-2.svg.import new file mode 100644 index 0000000..573d45f --- /dev/null +++ b/flumi/Assets/Icons/link-2.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://da33sld2tguf0" +path="res://.godot/imported/link-2.svg-65859ed5f57233280efa03e60ad3c6bb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/link-2.svg" +dest_files=["res://.godot/imported/link-2.svg-65859ed5f57233280efa03e60ad3c6bb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Scenes/BrowserMenus/download_entry.tscn b/flumi/Scenes/BrowserMenus/download_entry.tscn new file mode 100644 index 0000000..c66d66f --- /dev/null +++ b/flumi/Scenes/BrowserMenus/download_entry.tscn @@ -0,0 +1,106 @@ +[gd_scene load_steps=9 format=3 uid="uid://co3t7h2tavx10"] + +[ext_resource type="Script" uid="uid://c1qh0fqvvh8tq" path="res://Scripts/Browser/DownloadEntry.gd" id="1_script"] +[ext_resource type="Texture2D" uid="uid://cbwitcygwoqdo" path="res://Assets/Icons/download.svg" id="2_download"] +[ext_resource type="Texture2D" uid="uid://da33sld2tguf0" path="res://Assets/Icons/link-2.svg" id="3_goveu"] +[ext_resource type="Texture2D" uid="uid://d3ottyh38w3db" path="res://Assets/Icons/folder.svg" id="4_8t7bq"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8t7bq"] +content_margin_left = 10.0 +content_margin_top = 10.0 +content_margin_right = 10.0 +content_margin_bottom = 10.0 +bg_color = Color(0.202723, 0.202723, 0.202723, 1) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_8t7bq"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3j0oj"] +bg_color = Color(0.168627, 0.168627, 0.168627, 1) +corner_radius_top_left = 50 +corner_radius_top_right = 50 +corner_radius_bottom_right = 50 +corner_radius_bottom_left = 50 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4krvm"] +bg_color = Color(0.6, 0.6, 0.6, 0) +draw_center = false + +[node name="PanelContainer" type="PanelContainer"] +offset_right = 604.0 +offset_bottom = 24.0 +theme_override_styles/panel = SubResource("StyleBoxFlat_8t7bq") +script = ExtResource("1_script") + +[node name="DownloadEntry" type="HBoxContainer" parent="."] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="TimeLabel" type="RichTextLabel" parent="DownloadEntry"] +custom_minimum_size = Vector2(65, 0) +layout_mode = 2 +text = "2:00PM" +fit_content = true +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Spacer" type="Control" parent="DownloadEntry"] +custom_minimum_size = Vector2(15, 0) +layout_mode = 2 + +[node name="FileIcon" type="TextureRect" parent="DownloadEntry"] +custom_minimum_size = Vector2(24, 24) +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +texture = ExtResource("2_download") +expand_mode = 1 +stretch_mode = 5 + +[node name="FileNameLabel" type="RichTextLabel" parent="DownloadEntry"] +custom_minimum_size = Vector2(250, 0) +layout_mode = 2 +text = "example-file.pdf" +vertical_alignment = 1 + +[node name="LinkButton" type="Button" parent="DownloadEntry"] +custom_minimum_size = Vector2(32, 32) +layout_mode = 2 +theme_override_styles/focus = SubResource("StyleBoxEmpty_8t7bq") +theme_override_styles/hover = SubResource("StyleBoxFlat_3j0oj") +theme_override_styles/pressed = SubResource("StyleBoxFlat_4krvm") +theme_override_styles/normal = SubResource("StyleBoxFlat_4krvm") +icon = ExtResource("3_goveu") +icon_alignment = 1 + +[node name="FileButton" type="Button" parent="DownloadEntry"] +custom_minimum_size = Vector2(32, 32) +layout_mode = 2 +theme_override_styles/focus = SubResource("StyleBoxEmpty_8t7bq") +theme_override_styles/hover = SubResource("StyleBoxFlat_3j0oj") +theme_override_styles/pressed = SubResource("StyleBoxFlat_4krvm") +theme_override_styles/normal = SubResource("StyleBoxFlat_4krvm") +icon = ExtResource("4_8t7bq") +icon_alignment = 1 + +[node name="DomainLabel" type="RichTextLabel" parent="DownloadEntry"] +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +theme_override_colors/default_color = Color(0.817521, 0.817521, 0.817521, 1) +text = "example.com" +vertical_alignment = 1 + +[node name="SizeLabel" type="RichTextLabel" parent="DownloadEntry"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +theme_override_colors/default_color = Color(0.7, 0.7, 0.7, 1) +text = "2.5 MB" +fit_content = true +horizontal_alignment = 2 +vertical_alignment = 1 + +[connection signal="pressed" from="DownloadEntry/LinkButton" to="." method="_on_link_button_pressed"] +[connection signal="pressed" from="DownloadEntry/FileButton" to="." method="_on_file_button_pressed"] diff --git a/flumi/Scenes/BrowserMenus/downloads.tscn b/flumi/Scenes/BrowserMenus/downloads.tscn new file mode 100644 index 0000000..a3acd1c --- /dev/null +++ b/flumi/Scenes/BrowserMenus/downloads.tscn @@ -0,0 +1,88 @@ +[gd_scene load_steps=8 format=3 uid="uid://bxtm0d4q1w8vy"] + +[ext_resource type="Script" uid="uid://cp3geyxt0f8tn" path="res://Scripts/Browser/DownloadsStore.gd" id="1_w8vy"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_main"] +content_margin_left = 15.0 +content_margin_top = 15.0 +content_margin_right = 15.0 +content_margin_bottom = 15.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) +corner_radius_top_left = 30 +corner_radius_top_right = 30 +corner_radius_bottom_right = 30 +corner_radius_bottom_left = 30 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_search"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_search"] +bg_color = Color(0.168627, 0.168627, 0.168627, 1) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 +expand_margin_left = 40.0 + +[sub_resource type="Theme" id="Theme_search"] +LineEdit/styles/focus = SubResource("StyleBoxEmpty_search") +LineEdit/styles/normal = SubResource("StyleBoxFlat_search") + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_search_bg"] +content_margin_left = 10.0 +bg_color = Color(0.219501, 0.219501, 0.219501, 1) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_container"] +content_margin_left = 15.0 +content_margin_top = 15.0 +content_margin_right = 15.0 +content_margin_bottom = 5.0 +bg_color = Color(0.154876, 0.154876, 0.154876, 1) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 + +[node name="PopupPanel" type="PopupPanel"] +initial_position = 1 +size = Vector2i(770, 710) +visible = true +max_size = Vector2i(770, 710) +theme_override_styles/panel = SubResource("StyleBoxFlat_main") +script = ExtResource("1_w8vy") + +[node name="Main" type="VBoxContainer" parent="."] +custom_minimum_size = Vector2(710, 0) +offset_left = 15.0 +offset_top = 15.0 +offset_right = 755.0 +offset_bottom = 695.0 +size_flags_horizontal = 4 +theme_override_constants/separation = 15 + +[node name="LineEdit" type="LineEdit" parent="Main"] +custom_minimum_size = Vector2(0, 45) +layout_mode = 2 +size_flags_horizontal = 3 +theme = SubResource("Theme_search") +theme_override_styles/normal = SubResource("StyleBoxFlat_search_bg") +placeholder_text = "Search downloads..." +caret_blink = true + +[node name="PanelContainer2" type="PanelContainer" parent="Main"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_container") + +[node name="ScrollContainer" type="ScrollContainer" parent="Main/PanelContainer2"] +custom_minimum_size = Vector2(710, 500) +layout_mode = 2 +size_flags_vertical = 3 + +[node name="DownloadEntryContainer" type="VBoxContainer" parent="Main/PanelContainer2/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 diff --git a/flumi/Scenes/UI/DownloadDialog.tscn b/flumi/Scenes/UI/DownloadDialog.tscn new file mode 100644 index 0000000..6e03a55 --- /dev/null +++ b/flumi/Scenes/UI/DownloadDialog.tscn @@ -0,0 +1,118 @@ +[gd_scene load_steps=9 format=3 uid="uid://dq4q0exy5aeey"] + +[ext_resource type="Script" uid="uid://be6fpnhb4p57v" path="res://Scripts/Browser/DownloadDialog.gd" id="1_3v2kl"] +[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_0jrl0"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c7s3a"] +content_margin_left = 15.0 +content_margin_top = 15.0 +content_margin_right = 15.0 +content_margin_bottom = 15.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) +corner_radius_top_left = 30 +corner_radius_top_right = 30 +corner_radius_bottom_right = 30 +corner_radius_bottom_left = 30 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v0gbf"] +bg_color = Color(0.205117, 0.205117, 0.205117, 1) +corner_radius_top_left = 25 +corner_radius_top_right = 25 +corner_radius_bottom_right = 25 +corner_radius_bottom_left = 25 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t7gjp"] +draw_center = false +corner_radius_top_left = 25 +corner_radius_top_right = 25 +corner_radius_bottom_right = 25 +corner_radius_bottom_left = 25 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s5iik"] +bg_color = Color(0.34452, 0.55695, 0.888021, 1) +corner_radius_top_left = 25 +corner_radius_top_right = 25 +corner_radius_bottom_right = 25 +corner_radius_bottom_left = 25 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0jrl0"] +bg_color = Color(0.247059, 0.466667, 0.807843, 1) +corner_radius_top_left = 25 +corner_radius_top_right = 25 +corner_radius_bottom_right = 25 +corner_radius_bottom_left = 25 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cuj6o"] +bg_color = Color(0.247059, 0.466667, 0.807843, 1) +corner_radius_top_left = 25 +corner_radius_top_right = 25 +corner_radius_bottom_right = 25 +corner_radius_bottom_left = 25 + +[node name="PopupPanel" type="PopupPanel"] +initial_position = 1 +size = Vector2i(293, 140) +visible = true +wrap_controls = false +max_size = Vector2i(293, 140) +theme_override_styles/panel = SubResource("StyleBoxFlat_c7s3a") +script = ExtResource("1_3v2kl") + +[node name="VBox" type="VBoxContainer" parent="."] +custom_minimum_size = Vector2(240, 110) +offset_left = 15.0 +offset_top = 15.0 +offset_right = 278.0 +offset_bottom = 858.0 +size_flags_horizontal = 3 +size_flags_vertical = 4 + +[node name="FilenameLabel" type="Label" parent="VBox"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "File: example.png" +autowrap_mode = 3 + +[node name="URLLabel" type="Label" parent="VBox"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "From: example.com" +autowrap_mode = 3 + +[node name="HSeparator" type="HSeparator" parent="VBox"] +custom_minimum_size = Vector2(10, 20) +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBox"] +layout_mode = 2 +theme_override_constants/separation = 20 +alignment = 1 + +[node name="CancelButton" type="Button" parent="VBox/HBoxContainer"] +custom_minimum_size = Vector2(80, 35) +layout_mode = 2 +mouse_default_cursor_shape = 2 +theme = ExtResource("2_0jrl0") +theme_override_styles/hover = SubResource("StyleBoxFlat_v0gbf") +theme_override_styles/normal = SubResource("StyleBoxFlat_t7gjp") +text = "Deny" + +[node name="OkButton" type="Button" parent="VBox/HBoxContainer"] +custom_minimum_size = Vector2(80, 35) +layout_mode = 2 +mouse_default_cursor_shape = 2 +theme = ExtResource("2_0jrl0") +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_styles/hover = SubResource("StyleBoxFlat_s5iik") +theme_override_styles/pressed = SubResource("StyleBoxFlat_0jrl0") +theme_override_styles/normal = SubResource("StyleBoxFlat_cuj6o") +text = "Accept" + +[node name="FileDialog" type="FileDialog" parent="."] +access = 2 +use_native_dialog = true + +[connection signal="pressed" from="VBox/HBoxContainer/CancelButton" to="." method="_on_file_dialog_cancelled"] +[connection signal="pressed" from="VBox/HBoxContainer/OkButton" to="." method="_on_download_confirmed"] +[connection signal="canceled" from="FileDialog" to="." method="_on_file_dialog_cancelled"] +[connection signal="file_selected" from="FileDialog" to="." method="_on_save_location_selected"] diff --git a/flumi/Scenes/UI/DownloadProgress.tscn b/flumi/Scenes/UI/DownloadProgress.tscn new file mode 100644 index 0000000..71a2376 --- /dev/null +++ b/flumi/Scenes/UI/DownloadProgress.tscn @@ -0,0 +1,85 @@ +[gd_scene load_steps=8 format=3 uid="uid://b0gfxh1n7c5yu"] + +[ext_resource type="Script" uid="uid://kj6oda7sey5s" path="res://Scripts/Browser/DownloadProgress.gd" id="1_1a3vh"] +[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_lnt4f"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lnt4f"] +content_margin_left = 15.0 +content_margin_top = 15.0 +content_margin_right = 15.0 +content_margin_bottom = 15.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) +corner_radius_top_left = 25 +corner_radius_top_right = 25 +corner_radius_bottom_right = 25 +corner_radius_bottom_left = 25 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f51et"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.184314, 0.184314, 0.184314, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 +corner_detail = 6 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v5p1v"] +bg_color = Color(0.247059, 0.466667, 0.807843, 1) +corner_radius_top_left = 25 +corner_radius_top_right = 25 +corner_radius_bottom_right = 25 +corner_radius_bottom_left = 25 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_lnt4f"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cs7kk"] +bg_color = Color(0.15967, 0.15967, 0.15967, 1) +corner_radius_top_left = 30 +corner_radius_top_right = 30 +corner_radius_bottom_right = 30 +corner_radius_bottom_left = 30 + +[node name="DownloadProgress" type="PanelContainer"] +custom_minimum_size = Vector2(376, 80) +offset_right = 376.0 +offset_bottom = 81.0 +theme_override_styles/panel = SubResource("StyleBoxFlat_lnt4f") +script = ExtResource("1_1a3vh") + +[node name="HBox" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="VBox" type="VBoxContainer" parent="HBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="FilenameLabel" type="Label" parent="HBox/VBox"] +layout_mode = 2 +text = "Downloading file..." +text_overrun_behavior = 3 + +[node name="ProgressBar" type="ProgressBar" parent="HBox/VBox"] +layout_mode = 2 +theme = ExtResource("2_lnt4f") +theme_override_styles/background = SubResource("StyleBoxFlat_f51et") +theme_override_styles/fill = SubResource("StyleBoxFlat_v5p1v") +show_percentage = false + +[node name="StatusLabel" type="Label" parent="HBox/VBox"] +modulate = Color(0.7, 0.7, 0.7, 1) +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Preparing..." + +[node name="CancelButton" type="Button" parent="HBox"] +custom_minimum_size = Vector2(30, 30) +layout_mode = 2 +size_flags_vertical = 4 +theme_override_styles/focus = SubResource("StyleBoxEmpty_lnt4f") +theme_override_styles/hover = SubResource("StyleBoxFlat_cs7kk") +text = "โœ•" + +[connection signal="pressed" from="HBox/CancelButton" to="." method="_on_cancel_pressed"] diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd index b22f155..d46a369 100644 --- a/flumi/Scripts/B9/Lua.gd +++ b/flumi/Scripts/B9/Lua.gd @@ -744,6 +744,8 @@ func _handle_dom_operation(operation: Dictionary): LuaCanvasUtils.handle_canvas_setLineWidth(operation, dom_parser) "canvas_setFont": LuaCanvasUtils.handle_canvas_setFont(operation, dom_parser) + "request_download": + _handle_download_request(operation) _: pass # Unknown operation type, ignore @@ -990,3 +992,9 @@ func _notification(what: int): if timeout_manager: timeout_manager.cleanup_all_timeouts() threaded_vm.stop_lua_thread() + +func _handle_download_request(operation: Dictionary): + var download_data = operation.get("download_data", {}) + + var main_node = Engine.get_main_loop().current_scene + main_node.download_manager.handle_download_request(download_data) diff --git a/flumi/Scripts/Browser/DownloadDialog.gd b/flumi/Scripts/Browser/DownloadDialog.gd new file mode 100644 index 0000000..09cd4a9 --- /dev/null +++ b/flumi/Scripts/Browser/DownloadDialog.gd @@ -0,0 +1,98 @@ +class_name DownloadDialog +extends PopupPanel + +signal download_confirmed(download_data: Dictionary, save_path: String) +signal download_cancelled(download_data: Dictionary) + +@onready var ok_button: Button = $VBox/HBoxContainer/OkButton +@onready var cancel_button: Button = $VBox/HBoxContainer/CancelButton +@onready var file_dialog: FileDialog = $FileDialog + +@onready var filename_label: Label = $VBox/FilenameLabel +@onready var url_label: Label = $VBox/URLLabel + +var download_data: Dictionary = {} + +func show_download_dialog(data: Dictionary): + download_data = data + + var filename = data.get("filename", "download") + var url = data.get("url", "") + + filename_label.text = "File: " + filename + + var current_site = data.get("current_site", "") + if current_site != "": + url_label.text = "From: " + current_site + else: + url_label.text = "From: " + URLUtils.extract_domain(url) + + popup() + _animate_entrance() + ok_button.grab_focus() + +func _animate_entrance(): + if not is_inside_tree(): + return + + var original_size = Vector2(size) + var small_size = original_size * 0.8 + var size_difference = original_size - small_size + var original_pos = position + + size = Vector2i(small_size) + position = original_pos + Vector2i(size_difference * 0.5) + + var tween = create_tween() + if tween: + tween.set_parallel(true) + var size_property = tween.tween_property(self, "size", Vector2i(original_size), 0.2) + var pos_property = tween.tween_property(self, "position", original_pos, 0.2) + + if size_property: + size_property.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) + if pos_property: + pos_property.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) + +func _on_download_confirmed(): + file_dialog.current_file = download_data.get("filename", "download") + file_dialog.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) + file_dialog.show() + +func _animate_exit(): + if not is_inside_tree(): + queue_free() + return + + var current_size = Vector2(size) + var small_size = current_size * 0.8 + var size_difference = current_size - small_size + var current_pos = position + var target_pos = current_pos + Vector2i(size_difference * 0.5) + + var tween = create_tween() + if tween: + tween.set_parallel(true) + var size_property = tween.tween_property(self, "size", Vector2i(small_size), 0.15) + var pos_property = tween.tween_property(self, "position", target_pos, 0.15) + + if size_property: + size_property.set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK) + if pos_property: + pos_property.set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK) + + await tween.finished + + queue_free() + +func _on_save_location_selected(path: String): + download_confirmed.emit(download_data, path) + _animate_exit() + +func _on_file_dialog_cancelled(): + download_cancelled.emit(download_data) + _animate_exit() + +func _on_download_cancelled(): + download_cancelled.emit(download_data) + _animate_exit() diff --git a/flumi/Scripts/Browser/DownloadDialog.gd.uid b/flumi/Scripts/Browser/DownloadDialog.gd.uid new file mode 100644 index 0000000..e572ffb --- /dev/null +++ b/flumi/Scripts/Browser/DownloadDialog.gd.uid @@ -0,0 +1 @@ +uid://be6fpnhb4p57v diff --git a/flumi/Scripts/Browser/DownloadEntry.gd b/flumi/Scripts/Browser/DownloadEntry.gd new file mode 100644 index 0000000..4349a8f --- /dev/null +++ b/flumi/Scripts/Browser/DownloadEntry.gd @@ -0,0 +1,70 @@ +class_name DownloadEntry +extends PanelContainer + +@onready var time_label: RichTextLabel = $DownloadEntry/TimeLabel +@onready var filename_label: RichTextLabel = $DownloadEntry/FileNameLabel +@onready var domain_label: RichTextLabel = $DownloadEntry/DomainLabel +@onready var size_label: RichTextLabel = $DownloadEntry/SizeLabel +@onready var link_button: Button = $DownloadEntry/LinkButton +@onready var file_button: Button = $DownloadEntry/FileButton + +var download_data: Dictionary = {} + +func setup_download_entry(data: Dictionary): + download_data = data + + var filename = data.get("filename", "Unknown file") + var url = data.get("url", "") + var current_site = data.get("current_site", "") + var file_size = data.get("size", 0) + var timestamp = data.get("timestamp", Time.get_unix_time_from_system()) + + filename_label.text = filename + + current_site = URLUtils.extract_domain(url) if url != "" else "Unknown source" + domain_label.text = current_site + + var size_text = NetworkRequest.format_bytes(file_size) + size_label.text = size_text + + var time_text = _format_time(timestamp) + time_label.text = time_text + +func _format_time(unix_timestamp: float) -> String: + var datetime = Time.get_datetime_dict_from_unix_time(int(unix_timestamp)) + + # Format as "3:45PM" + var hour = datetime.hour + var minute = datetime.minute + var am_pm = "AM" + + if hour == 0: + hour = 12 + elif hour > 12: + hour -= 12 + am_pm = "PM" + elif hour == 12: + am_pm = "PM" + + return "%d:%02d%s" % [hour, minute, am_pm] + +func get_filename() -> String: + return download_data.get("filename", "") + +func get_domain() -> String: + var url = download_data.get("url", "") + return URLUtils.extract_domain(url) if url != "" else "" + +func get_download_data() -> Dictionary: + return download_data + +func _on_link_button_pressed() -> void: + DisplayServer.clipboard_set(download_data.get("url", "")) + +func _on_file_button_pressed() -> void: + var file_path = download_data.get("file_path", "") + if file_path != "" and FileAccess.file_exists(file_path): + var file_dir = file_path.get_base_dir() + OS.shell_show_in_file_manager(file_dir) + else: + print("File not found: ", file_path) diff --git a/flumi/Scripts/Browser/DownloadEntry.gd.uid b/flumi/Scripts/Browser/DownloadEntry.gd.uid new file mode 100644 index 0000000..645cd66 --- /dev/null +++ b/flumi/Scripts/Browser/DownloadEntry.gd.uid @@ -0,0 +1 @@ +uid://c1qh0fqvvh8tq diff --git a/flumi/Scripts/Browser/DownloadManager.gd b/flumi/Scripts/Browser/DownloadManager.gd new file mode 100644 index 0000000..35d9b2f --- /dev/null +++ b/flumi/Scripts/Browser/DownloadManager.gd @@ -0,0 +1,211 @@ +class_name DownloadManager +extends Node + +const DOWNLOAD_DIALOG = preload("res://Scenes/UI/DownloadDialog.tscn") +const DOWNLOAD_PROGRESS = preload("res://Scenes/UI/DownloadProgress.tscn") +const DOWNLOADS_HISTORY = preload("res://Scenes/BrowserMenus/downloads.tscn") + +var active_downloads: Dictionary = {} +var download_progress_container: VBoxContainer = null +var downloads_history_ui: DownloadsStore = null +var main_node: Main = null + +func _init(main_reference: Main): + main_node = main_reference + +func _ensure_download_progress_container(): + if not download_progress_container: + download_progress_container = VBoxContainer.new() + download_progress_container.name = "DownloadProgressContainer" + download_progress_container.size_flags_horizontal = Control.SIZE_SHRINK_END + download_progress_container.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + + var anchor_container = Control.new() + anchor_container.name = "DownloadAnchor" + anchor_container.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT) + anchor_container.position = Vector2(0, 130) + anchor_container.offset_left = 381 # 376 + 5px padding + anchor_container.add_child(download_progress_container) + main_node.add_child(anchor_container) + +func handle_download_request(download_data: Dictionary): + print("Download requested: ", download_data) + + var active_tab = main_node.get_active_tab() + if active_tab and active_tab.current_url: + download_data["current_site"] = URLUtils.extract_domain(active_tab.current_url) + else: + download_data["current_site"] = "Unknown site" + + var dialog = DOWNLOAD_DIALOG.instantiate() + main_node.add_child(dialog) + + dialog.download_confirmed.connect(_on_download_confirmed) + dialog.download_cancelled.connect(_on_download_cancelled) + dialog.show_download_dialog(download_data) + +func _on_download_confirmed(download_data: Dictionary, save_path: String): + var download_id = download_data.get("id", "") + var url = download_data.get("url", "") + print(download_id, url) + if download_id.is_empty() or url.is_empty(): + push_error("Invalid download data") + return + + _start_download(download_id, url, save_path, download_data) + +func _on_download_cancelled(download_data: Dictionary): + print("Download cancelled: ", download_data.get("filename", "Unknown")) + +func _start_download(download_id: String, url: String, save_path: String, download_data: Dictionary): + _ensure_download_progress_container() + + var progress_ui = DOWNLOAD_PROGRESS.instantiate() + + download_progress_container.add_child(progress_ui) + + progress_ui.setup_download(download_id, download_data) + progress_ui.download_cancelled.connect(_on_download_progress_cancelled) + + var http_request = HTTPRequest.new() + http_request.name = "DownloadRequest_" + download_id + main_node.add_child(http_request) + + active_downloads[download_id] = { + "http_request": http_request, + "save_path": save_path, + "progress_ui": progress_ui, + "start_time": Time.get_ticks_msec() / 1000.0, + "total_bytes": 0, + "downloaded_bytes": 0, + "url": download_data.get("url", ""), + "filename": download_data.get("filename", ""), + "current_site": download_data.get("current_site", "") + } + + http_request.set_download_file(save_path) + + http_request.request_completed.connect(func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + _on_download_completed(download_id, result, response_code, headers, body) + ) + + var headers = ["User-Agent: GURT Browser 1.0"] + var request_error = http_request.request(url, headers) + + if request_error != OK: + var error_msg = "Failed to start download: " + str(request_error) + print(error_msg) + if progress_ui: + progress_ui.set_error(error_msg) + http_request.queue_free() + active_downloads.erase(download_id) + return + + var timer = Timer.new() + timer.name = "ProgressTimer_" + download_id + timer.wait_time = 0.5 + timer.timeout.connect(func(): _update_download_progress(download_id)) + main_node.add_child(timer) + timer.start() + +func _update_download_progress(download_id: String): + if not active_downloads.has(download_id): + return + + var download_info = active_downloads[download_id] + var http_request = download_info.http_request + var progress_ui = download_info.progress_ui + + if http_request and progress_ui: + var downloaded = http_request.get_downloaded_bytes() + var total = http_request.get_body_size() + + download_info.downloaded_bytes = downloaded + download_info.total_bytes = total + + var progress_percent = 0.0 + if total > 0: + progress_percent = (float(downloaded) / float(total)) * 100.0 + + progress_ui.update_progress(progress_percent, downloaded, total) + +func _on_download_completed(download_id: String, result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + if not active_downloads.has(download_id): + return + + var download_info = active_downloads[download_id] + var progress_ui = download_info.progress_ui + var save_path = download_info.save_path + + var timer = main_node.get_node_or_null("ProgressTimer_" + download_id) + if timer: + timer.queue_free() + + if response_code >= 200 and response_code < 300 and result == HTTPRequest.RESULT_SUCCESS: + var file = FileAccess.open(save_path, FileAccess.READ) + if file: + var file_size = file.get_length() + file.close() + + if progress_ui: + progress_ui.set_completed(save_path) + + _add_to_download_history(download_info, file_size, save_path) + + print("Download completed: ", save_path) + else: + if progress_ui: + progress_ui.set_error("Downloaded file not found") + print("Downloaded file not found: ", save_path) + else: + var error_msg = "HTTP " + str(response_code) if response_code >= 400 else "Request failed (" + str(result) + ")" + if progress_ui: + progress_ui.set_error(error_msg) + print("Download failed: ", error_msg) + + if FileAccess.file_exists(save_path): + DirAccess.remove_absolute(save_path) + + download_info.http_request.queue_free() + active_downloads.erase(download_id) + +func _on_download_progress_cancelled(download_id: String): + if not active_downloads.has(download_id): + return + + var download_info = active_downloads[download_id] + + if download_info.http_request: + download_info.http_request.cancel_request() + download_info.http_request.queue_free() + + var timer = main_node.get_node_or_null("ProgressTimer_" + download_id) + if timer: + timer.queue_free() + + active_downloads.erase(download_id) + print("Download cancelled: ", download_id) + +func show_downloads_history(): + _ensure_downloads_history_ui() + downloads_history_ui.popup_centered_ratio(0.8) + +func _add_to_download_history(download_info: Dictionary, file_size: int, file_path: String): + _ensure_downloads_history_ui() + + var history_data = { + "url": download_info.url, + "filename": download_info.filename, + "size": file_size, + "timestamp": Time.get_unix_time_from_system(), + "file_path": file_path, + "current_site": download_info.get("current_site", "") + } + + downloads_history_ui.add_download_entry(history_data) + +func _ensure_downloads_history_ui(): + if not downloads_history_ui: + downloads_history_ui = DOWNLOADS_HISTORY.instantiate() + downloads_history_ui.visible = false + main_node.add_child(downloads_history_ui) diff --git a/flumi/Scripts/Browser/DownloadManager.gd.uid b/flumi/Scripts/Browser/DownloadManager.gd.uid new file mode 100644 index 0000000..0690ba1 --- /dev/null +++ b/flumi/Scripts/Browser/DownloadManager.gd.uid @@ -0,0 +1 @@ +uid://omjynclhfe8u diff --git a/flumi/Scripts/Browser/DownloadProgress.gd b/flumi/Scripts/Browser/DownloadProgress.gd new file mode 100644 index 0000000..9ca3d13 --- /dev/null +++ b/flumi/Scripts/Browser/DownloadProgress.gd @@ -0,0 +1,129 @@ +class_name DownloadProgress +extends PanelContainer + +signal download_cancelled(download_id: String) + +@onready var filename_label: Label = $HBox/VBox/FilenameLabel +@onready var progress_bar: ProgressBar = $HBox/VBox/ProgressBar +@onready var status_label: Label = $HBox/VBox/StatusLabel +@onready var cancel_button: Button = $HBox/CancelButton + +var download_id: String = "" +var download_data: Dictionary = {} +var start_time: float = 0.0 + +func _ready(): + progress_bar.value = 0 + status_label.text = "Starting download..." + +func setup_download(id: String, data: Dictionary): + download_id = id + download_data = data + start_time = Time.get_ticks_msec() / 1000.0 + + var filename = data.get("filename", "Unknown file") + filename_label.text = filename + status_label.text = "Starting download..." + progress_bar.value = 0 + + _animate_entrance() + +func _animate_entrance(): + if not is_inside_tree(): + return + + var download_container = get_parent() + var anchor_container = download_container.get_parent() if download_container else null + + if anchor_container and anchor_container.name == "DownloadAnchor" and download_container.get_child_count() == 1: + var tween = create_tween() + if tween: + var tween_property = tween.tween_property(anchor_container, "offset_left", -381, 0.3) + if tween_property: + tween_property.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) + else: + call_deferred("_animate_individual_entrance") + +func _animate_individual_entrance(): + if not is_inside_tree(): + return + + var original_x = position.x + position.x += 400 # Move off-screen to the right + + var tween = create_tween() + if tween: + var slide_property = tween.tween_property(self, "position:x", original_x, 0.3) + if slide_property: + slide_property.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) + +func update_progress(progress_percent: float, bytes_downloaded: int = 0, total_bytes: int = 0): + progress_bar.value = progress_percent + + var elapsed_time = (Time.get_ticks_msec() / 1000.0) - start_time + var status_text = "" + + if total_bytes > 0: + var speed_bps = bytes_downloaded / elapsed_time if elapsed_time > 0 else 0 + + var remaining_bytes = total_bytes - bytes_downloaded + var eta_seconds = remaining_bytes / speed_bps if speed_bps > 0 else 0 + + status_text = NetworkRequest.format_bytes(bytes_downloaded) + " / " + NetworkRequest.format_bytes(total_bytes) + if speed_bps > 0: + status_text += " (" + NetworkRequest.format_bytes(int(speed_bps)) + "/s)" + if eta_seconds > 0 and eta_seconds < 3600: + status_text += " - %d seconds left" % int(eta_seconds) + else: + status_text = "%.0f%% complete" % progress_percent + + status_label.text = status_text + +func set_completed(file_path: String): + progress_bar.value = 100 + status_label.text = "Download complete: " + file_path.get_file() + cancel_button.text = "โœ“" + cancel_button.disabled = true + + await get_tree().create_timer(2.0).timeout + _animate_exit() + +func set_error(error_message: String): + status_label.text = "Error: " + error_message + cancel_button.text = "โœ•" + progress_bar.modulate = Color.RED + + await get_tree().create_timer(4.0).timeout + _animate_exit() + +func _animate_exit(): + if not is_inside_tree(): + queue_free() + return + + var download_container = get_parent() + var anchor_container = download_container.get_parent() if download_container else null + + var is_last_download = download_container and download_container.get_child_count() == 1 + + var tween = create_tween() + if tween: + var slide_property = tween.tween_property(self, "position:x", position.x + 400, 0.25) + if slide_property: + slide_property.set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK) + + await tween.finished + + if is_last_download and anchor_container and anchor_container.name == "DownloadAnchor": + var container_tween = create_tween() + if container_tween: + var container_property = container_tween.tween_property(anchor_container, "offset_left", 381, 0.25) + if container_property: + container_property.set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK) + await container_tween.finished + + queue_free() + +func _on_cancel_pressed(): + download_cancelled.emit(download_id) + _animate_exit() diff --git a/flumi/Scripts/Browser/DownloadProgress.gd.uid b/flumi/Scripts/Browser/DownloadProgress.gd.uid new file mode 100644 index 0000000..36055f4 --- /dev/null +++ b/flumi/Scripts/Browser/DownloadProgress.gd.uid @@ -0,0 +1 @@ +uid://kj6oda7sey5s diff --git a/flumi/Scripts/Browser/DownloadsStore.gd b/flumi/Scripts/Browser/DownloadsStore.gd new file mode 100644 index 0000000..ac401c1 --- /dev/null +++ b/flumi/Scripts/Browser/DownloadsStore.gd @@ -0,0 +1,76 @@ +class_name DownloadsStore +extends PopupPanel + +@onready var search_line_edit: LineEdit = $Main/LineEdit +@onready var download_entry_container: VBoxContainer = $Main/PanelContainer2/ScrollContainer/DownloadEntryContainer + +const DOWNLOAD_ENTRY = preload("res://Scenes/BrowserMenus/download_entry.tscn") +var save_path = "user://downloads_history.json" + +var download_entries: Array[DownloadEntry] = [] + +func _ready(): + search_line_edit.text_changed.connect(_on_search_text_changed) + + load_download_history() + +func add_download_entry(download_data: Dictionary): + var entry = DOWNLOAD_ENTRY.instantiate() + download_entry_container.add_child(entry) + + entry.setup_download_entry(download_data) + + download_entries.append(entry) + + save_download_history() + +func _on_search_text_changed(new_text: String): + var search_term = new_text.to_lower().strip_edges() + + for entry in download_entries: + if search_term.is_empty(): + entry.visible = true + else: + var filename = entry.get_filename().to_lower() + var domain = entry.get_domain().to_lower() + entry.visible = filename.contains(search_term) or domain.contains(search_term) + +func load_download_history(): + if not FileAccess.file_exists(save_path): + return + + var file = FileAccess.open(save_path, FileAccess.READ) + if not file: + print("Could not open downloads history file for reading") + return + + var json_text = file.get_as_text() + file.close() + + var json = JSON.new() + var parse_result = json.parse(json_text) + if parse_result != OK: + print("Error parsing downloads history JSON") + return + + var downloads_data = json.data + if downloads_data is Array: + for download_data in downloads_data: + add_download_entry(download_data) + +func save_download_history(): + var file = FileAccess.open(save_path, FileAccess.WRITE) + if not file: + print("Could not open downloads history file for writing") + return + + var downloads_data = get_download_data_array() + var json_text = JSON.stringify(downloads_data) + file.store_string(json_text) + file.close() + +func get_download_data_array() -> Array[Dictionary]: + var data_array: Array[Dictionary] = [] + for entry in download_entries: + data_array.append(entry.get_download_data()) + return data_array diff --git a/flumi/Scripts/Browser/DownloadsStore.gd.uid b/flumi/Scripts/Browser/DownloadsStore.gd.uid new file mode 100644 index 0000000..ab1dfa2 --- /dev/null +++ b/flumi/Scripts/Browser/DownloadsStore.gd.uid @@ -0,0 +1 @@ +uid://cp3geyxt0f8tn diff --git a/flumi/Scripts/Browser/OptionButton.gd b/flumi/Scripts/Browser/OptionButton.gd index 13e6ed5..62f7321 100644 --- a/flumi/Scripts/Browser/OptionButton.gd +++ b/flumi/Scripts/Browser/OptionButton.gd @@ -25,6 +25,10 @@ func _input(_event: InputEvent) -> void: # CTRL+H - History _on_options_menu_id_pressed(4) get_viewport().set_input_as_handled() + elif _event.keycode == KEY_J: + # CTRL+J - Downloads + _on_options_menu_id_pressed(5) + get_viewport().set_input_as_handled() func _on_options_menu_id_pressed(id: int) -> void: if id == 0: # new tab @@ -36,6 +40,8 @@ func _on_options_menu_id_pressed(id: int) -> void: OS.create_process(OS.get_executable_path(), ["--incognito"]) if id == 4: # history show_history() + if id == 5: # downloads + show_downloads() if id == 10: # exit get_tree().quit() @@ -53,3 +59,6 @@ func show_history() -> void: func _on_history_closed() -> void: if history_scene: history_scene.hide() + +func show_downloads() -> void: + main.download_manager.show_downloads_history() diff --git a/flumi/Scripts/Engine/FontManager.gd b/flumi/Scripts/Engine/FontManager.gd index 8120cd0..a2adecc 100644 --- a/flumi/Scripts/Engine/FontManager.gd +++ b/flumi/Scripts/Engine/FontManager.gd @@ -40,7 +40,7 @@ static func load_web_font(font_info: Dictionary) -> void: http_request.timeout = 30.0 http_request.request_completed.connect(func(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray): - if response_code == 200: + if response_code >= 200 and response_code < 300: if body.size() > 0: var font = FontFile.new() diff --git a/flumi/Scripts/Tags/audio.gd b/flumi/Scripts/Tags/audio.gd index 3786ee8..bc8b23a 100644 --- a/flumi/Scripts/Tags/audio.gd +++ b/flumi/Scripts/Tags/audio.gd @@ -122,7 +122,7 @@ func _on_audio_download_completed(_result: int, response_code: int, headers: Pac var http_request = get_children().filter(func(child): return child is HTTPRequest)[0] http_request.queue_free() - if response_code != 200: + if response_code < 200 or response_code >= 300: return if body.size() == 0: diff --git a/flumi/Scripts/Utils/Lua/Download.gd b/flumi/Scripts/Utils/Lua/Download.gd new file mode 100644 index 0000000..a2ca1bf --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Download.gd @@ -0,0 +1,65 @@ +class_name LuaDownloadUtils +extends RefCounted + +static var last_user_event_time: int = 0 +static var user_event_window_ms: int = 100 +static var next_download_id: int = 1 + +static func setup_download_api(vm: LuauVM): + vm.lua_pushcallable(_lua_download_handler, "gurt.download") + vm.lua_getglobal("gurt") + if vm.lua_isnil(-1): + vm.lua_pop(1) + vm.lua_newtable() + vm.lua_setglobal("gurt") + vm.lua_getglobal("gurt") + + vm.lua_pushvalue(-2) + vm.lua_setfield(-2, "download") + vm.lua_pop(2) + +static func mark_user_event(): + last_user_event_time = Time.get_ticks_msec() + +static func _check_if_likely_user_event(current_time: int) -> bool: + var time_since_user_event = current_time - last_user_event_time + return time_since_user_event < user_event_window_ms + +static func _lua_download_handler(vm: LuauVM) -> int: + var url: String = vm.luaL_checkstring(1) + var filename: String = "" + + if vm.lua_gettop() >= 2 and not vm.lua_isnil(2): + filename = vm.luaL_checkstring(2) + else: + filename = url.get_file() + if filename.is_empty(): + filename = "download" + + var current_time = Time.get_ticks_msec() + var is_likely_user_event = _check_if_likely_user_event(current_time) + + if not is_likely_user_event: + vm.luaL_error("Download can only be called from within a user interaction (like a click event)") + return 0 + + var download_id = "download_" + str(next_download_id) + next_download_id += 1 + + var download_data = { + "id": download_id, + "url": url, + "filename": filename, + "timestamp": Time.get_unix_time_from_system() + } + + var lua_api = vm.get_meta("lua_api") as LuaAPI + if lua_api: + var operation = { + "type": "request_download", + "download_data": download_data + } + lua_api.call_deferred("_handle_dom_operation", operation) + + vm.lua_pushstring(download_id) + return 1 diff --git a/flumi/Scripts/Utils/Lua/Download.gd.uid b/flumi/Scripts/Utils/Lua/Download.gd.uid new file mode 100644 index 0000000..8853229 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Download.gd.uid @@ -0,0 +1 @@ +uid://b3nu8c4qlxj45 diff --git a/flumi/Scripts/Utils/Lua/Event.gd b/flumi/Scripts/Utils/Lua/Event.gd index 73a77c3..260925b 100644 --- a/flumi/Scripts/Utils/Lua/Event.gd +++ b/flumi/Scripts/Utils/Lua/Event.gd @@ -16,6 +16,7 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri if signal_node.has_signal("pressed"): var wrapper = func(): LuaAudioUtils.mark_user_event() + LuaDownloadUtils.mark_user_event() subscription.lua_api._on_event_triggered(subscription) signal_node.pressed.connect(wrapper) subscription.connected_signal = "pressed" @@ -25,6 +26,7 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri elif signal_node is Control: var wrapper = func(event: InputEvent): LuaAudioUtils.mark_user_event() + LuaDownloadUtils.mark_user_event() subscription.lua_api._on_gui_input_click(event, subscription) signal_node.gui_input.connect(wrapper) subscription.connected_signal = "gui_input" diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd index 7b2fde5..3d3ed40 100644 --- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd @@ -379,6 +379,7 @@ func _setup_additional_lua_apis(): LuaJSONUtils.setup_json_api(lua_vm) LuaWebSocketUtils.setup_websocket_api(lua_vm) LuaAudioUtils.setup_audio_api(lua_vm) + LuaDownloadUtils.setup_download_api(lua_vm) LuaCrumbsUtils.setup_crumbs_api(lua_vm) LuaRegexUtils.setup_regex_api(lua_vm) LuaURLUtils.setup_url_api(lua_vm) diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 0a5ee42..102a13f 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -35,6 +35,8 @@ const AUDIO = preload("res://Scenes/Tags/audio.tscn") const POSTPROCESS = preload("res://Scenes/Tags/postprocess.tscn") const CANVAS = preload("res://Scenes/Tags/canvas.tscn") +const DOWNLOAD_MANAGER = preload("res://Scripts/Browser/DownloadManager.gd") + const MIN_SIZE = Vector2i(750, 200) var font_dependent_elements: Array = [] @@ -42,6 +44,7 @@ var current_domain = "" var main_navigation_request: NetworkRequest = null var network_start_time: float = 0.0 var network_end_time: float = 0.0 +var download_manager: DownloadManager = null func should_group_as_inline(element: HTMLParser.HTMLElement) -> bool: if element.tag_name == "input": @@ -61,6 +64,9 @@ func _ready(): CertificateManager.initialize() + download_manager = DOWNLOAD_MANAGER.new(self) + add_child(download_manager) + var original_scroll = website_container.get_parent() if original_scroll: original_scroll.visible = false diff --git a/tests/download.html b/tests/download.html new file mode 100644 index 0000000..30c4581 --- /dev/null +++ b/tests/download.html @@ -0,0 +1,159 @@ + + Download API Demo + + + + + + + + + + +

๐Ÿ“ฅ Download API Demo

+ +
+
+

Download API Usage:

+

local downloadId = gurt.download(url, filename)

+

Returns a unique download ID that can be used to track the download progress.

+
+ +
+

Download API Test

+

Click the buttons below to test the download functionality:

+
+ + + +
+
+ +
+ +
+gurt.select("#download-image"):on("click", function() + local downloadId = gurt.download("https://httpbin.org/image/png", "test-image.png") + print("Started download:", downloadId) +end) + +gurt.select("#download-text"):on("click", function() + local downloadId = gurt.download("https://httpbin.org/robots.txt", "robots.txt") + print("Started text download:", downloadId) +end) + +gurt.select("#download-json"):on("click", function() + local downloadId = gurt.download("https://httpbin.org/json", "sample-data.json") + print("Started JSON download:", downloadId) +end) + +gurt.select("#download-shit"):on("click", function() + local downloadId = gurt.download("https://releases.ubuntu.com/24.04.3/ubuntu-24.04.3-desktop-amd64.iso", "linux.iso") +end) +
+
+ +

Download Log

+
+ +
+
+
Initializing...
+
+ +
+

Download API Features:

+
    +
  • gurt.download(url, filename): Initiates a download from the given URL
  • +
  • Return Value: Returns a unique download ID for tracking
  • +
  • File Types: Supports any file type (images, text, binary, etc.)
  • +
  • Large Files: Can handle large downloads like OS images
  • +
+

Test Cases:

+
    +
  • Image Download: PNG image from httpbin.org
  • +
  • Text Download: robots.txt file
  • +
  • JSON Download: Sample JSON data
  • +
  • Large File: Ubuntu 24.04.3 ISO (~5GB) - Use with caution!
  • +
+
+
+