diff --git a/addons/gd-plug/plug.gd.uid b/addons/gd-plug/plug.gd.uid index aea56d7c..06d64b9f 100644 --- a/addons/gd-plug/plug.gd.uid +++ b/addons/gd-plug/plug.gd.uid @@ -1 +1,2 @@ -uid://c5c5bkysqj17q +uid://1f0e7lwytq05 + diff --git a/addons/godot_xml/xml.gd.uid b/addons/godot_xml/xml.gd.uid index cff8b029..58c2adec 100644 --- a/addons/godot_xml/xml.gd.uid +++ b/addons/godot_xml/xml.gd.uid @@ -1 +1,2 @@ -uid://cbpe175luqeh5 +uid://chr0ovfgn2bi8 + diff --git a/addons/godot_xml/xml_document.gd.uid b/addons/godot_xml/xml_document.gd.uid index a4eb7ba7..6525b450 100644 --- a/addons/godot_xml/xml_document.gd.uid +++ b/addons/godot_xml/xml_document.gd.uid @@ -1 +1,2 @@ -uid://m0cqleolr6wu +uid://32d2di2afat7 + diff --git a/addons/godot_xml/xml_node.gd.uid b/addons/godot_xml/xml_node.gd.uid index c2911bb8..2da441df 100644 --- a/addons/godot_xml/xml_node.gd.uid +++ b/addons/godot_xml/xml_node.gd.uid @@ -1 +1,2 @@ -uid://dnhl7npih03cw +uid://bx72q2ut6c4ky + diff --git a/addons/use-context/context.gd.uid b/addons/use-context/context.gd.uid index c325119a..d74bac9c 100644 --- a/addons/use-context/context.gd.uid +++ b/addons/use-context/context.gd.uid @@ -1 +1,2 @@ -uid://cbo28vhpclmcw +uid://dw6nnxtgk0reu + diff --git a/addons/use-context/context_keys.gd.uid b/addons/use-context/context_keys.gd.uid index d64b29ba..a5c5d6c3 100644 --- a/addons/use-context/context_keys.gd.uid +++ b/addons/use-context/context_keys.gd.uid @@ -1 +1,2 @@ -uid://2qpeoq0o2o6w +uid://dtssx16d07myw + diff --git a/addons/use-context/context_node.gd.uid b/addons/use-context/context_node.gd.uid index 26dd40f8..ff977d58 100644 --- a/addons/use-context/context_node.gd.uid +++ b/addons/use-context/context_node.gd.uid @@ -1 +1,2 @@ -uid://cf715lgpc0fqr +uid://c81coxv48a5x8 + diff --git a/addons/use-context/plugin.gd.uid b/addons/use-context/plugin.gd.uid index 621739ff..b86d7da9 100644 --- a/addons/use-context/plugin.gd.uid +++ b/addons/use-context/plugin.gd.uid @@ -1 +1,2 @@ -uid://bl6n4ko8nvnxd +uid://c2nfcywog2uu5 + diff --git a/addons/uuid.gd.uid b/addons/uuid.gd.uid index 2d07bf0a..2ce7d241 100644 --- a/addons/uuid.gd.uid +++ b/addons/uuid.gd.uid @@ -1 +1,2 @@ -uid://djx3batsmvwql +uid://ceukf5aotcrbk + diff --git a/plug.gd.uid b/plug.gd.uid index 7f277716..55883008 100644 --- a/plug.gd.uid +++ b/plug.gd.uid @@ -1 +1,2 @@ -uid://3phu8m5awfbo +uid://dvse2dunn1x0r + diff --git a/project.godot b/project.godot index 3f9243e9..4109f5b3 100644 --- a/project.godot +++ b/project.godot @@ -8,13 +8,17 @@ config_version=5 +[animation] + +compatibility/default_parent_skeleton_in_mesh_instance_3d=true + [application] config/name="Godots" config/description="Ultimate go-to hub for managing your Godot versions and projects!" config/tags=PackedStringArray("application") run/main_scene="res://src/main/main.tscn" -config/features=PackedStringArray("4.5", "GL Compatibility") +config/features=PackedStringArray("4.6", "GL Compatibility") run/low_processor_mode=true boot_splash/bg_color=Color(0.113725, 0.133333, 0.160784, 1) config/icon="res://icon.svg" @@ -26,7 +30,7 @@ Output="*res://src/output.gd" AutoClose="*res://src/auto_close.gd" Cache="*res://src/cache.gd" HttpClient="*res://src/http_client.gd" -Context="*res://addons/use-context/context_node.gd" +Context="*uid://c81coxv48a5x8" AutoNativeFileDialog="*res://src/auto_native_file_dialog.gd" [debug] @@ -47,7 +51,7 @@ window/subwindows/embed_subwindows=false [editor_plugins] -enabled=PackedStringArray("res://addons/use-context/plugin.cfg") +enabled=PackedStringArray("res://addons/use-context/plugin.cfg", "res://addons/gdUnit4/plugin.cfg") [filesystem] diff --git a/src/auto_close.gd.uid b/src/auto_close.gd.uid index f215ea07..6777bff1 100644 --- a/src/auto_close.gd.uid +++ b/src/auto_close.gd.uid @@ -1 +1,2 @@ -uid://b1rn3gnm4lk16 +uid://cp0m3dlq17g6r + diff --git a/src/auto_native_file_dialog.gd.uid b/src/auto_native_file_dialog.gd.uid index 9a12d9f9..c6f35fb7 100644 --- a/src/auto_native_file_dialog.gd.uid +++ b/src/auto_native_file_dialog.gd.uid @@ -1 +1,2 @@ -uid://cnicqwcv4afa3 +uid://c08027ctcwjcc + diff --git a/src/cache.gd.uid b/src/cache.gd.uid index 0ab849b3..42727743 100644 --- a/src/cache.gd.uid +++ b/src/cache.gd.uid @@ -1 +1,2 @@ -uid://cdl24h7qhwmwe +uid://cjfop6t2ghgt8 + diff --git a/src/cli/cli_command.gd.uid b/src/cli/cli_command.gd.uid index 9ed262ed..090616cd 100644 --- a/src/cli/cli_command.gd.uid +++ b/src/cli/cli_command.gd.uid @@ -1 +1,2 @@ -uid://bpn5wen2ymhfn +uid://5wk21palm2fr + diff --git a/src/cli/cli_option.gd.uid b/src/cli/cli_option.gd.uid index d214dbd0..1d39cab8 100644 --- a/src/cli/cli_option.gd.uid +++ b/src/cli/cli_option.gd.uid @@ -1 +1,2 @@ -uid://d2ka7che4c51t +uid://2jaf8phogpok + diff --git a/src/cli/commands/default_routes.gd.uid b/src/cli/commands/default_routes.gd.uid index 366a9f19..19e91184 100644 --- a/src/cli/commands/default_routes.gd.uid +++ b/src/cli/commands/default_routes.gd.uid @@ -1 +1,2 @@ -uid://rt1cn2sek6er +uid://dg601c6a0vc4e + diff --git a/src/cli/commands/editor/editor_routes.gd.uid b/src/cli/commands/editor/editor_routes.gd.uid index a195a554..d457f36a 100644 --- a/src/cli/commands/editor/editor_routes.gd.uid +++ b/src/cli/commands/editor/editor_routes.gd.uid @@ -1 +1,2 @@ -uid://drrpv7gu17f6d +uid://bemmsipsrle6y + diff --git a/src/cli/commands/editor/list/list.gd.uid b/src/cli/commands/editor/list/list.gd.uid index 329e203c..c95fad1e 100644 --- a/src/cli/commands/editor/list/list.gd.uid +++ b/src/cli/commands/editor/list/list.gd.uid @@ -1 +1,2 @@ -uid://dgexyyybo084g +uid://c88mn61jde3lb + diff --git a/src/cli/commands/exec/exec.gd.uid b/src/cli/commands/exec/exec.gd.uid index d7eecc65..bab408cd 100644 --- a/src/cli/commands/exec/exec.gd.uid +++ b/src/cli/commands/exec/exec.gd.uid @@ -1 +1,2 @@ -uid://1qlxwxdp08qh +uid://dyh0bnl5nppt + diff --git a/src/cli/commands/help/help.gd.uid b/src/cli/commands/help/help.gd.uid index 39ad6840..3c4bb383 100644 --- a/src/cli/commands/help/help.gd.uid +++ b/src/cli/commands/help/help.gd.uid @@ -1 +1,2 @@ -uid://ccgs4schictx6 +uid://daugvpgojekxm + diff --git a/src/cli/commands/project/open/open_recent_project.gd.uid b/src/cli/commands/project/open/open_recent_project.gd.uid index fb51f35a..1b20caee 100644 --- a/src/cli/commands/project/open/open_recent_project.gd.uid +++ b/src/cli/commands/project/open/open_recent_project.gd.uid @@ -1 +1,2 @@ -uid://bx0e16xw8meov +uid://dgiaxwn0jyvu + diff --git a/src/cli/ctx.gd.uid b/src/cli/ctx.gd.uid index 627280bf..c48f958d 100644 --- a/src/cli/ctx.gd.uid +++ b/src/cli/ctx.gd.uid @@ -1 +1,2 @@ -uid://d3i21wg7bdvoh +uid://nevdhma1n7qw + diff --git a/src/cli/godots_commands.gd.uid b/src/cli/godots_commands.gd.uid index 73a58a53..3437bfea 100644 --- a/src/cli/godots_commands.gd.uid +++ b/src/cli/godots_commands.gd.uid @@ -1 +1,2 @@ -uid://dyjqhkbemjwp +uid://ola1p0phop6e + diff --git a/src/cli/parser/cli_grammar.gd.uid b/src/cli/parser/cli_grammar.gd.uid index 00d920c1..b9886f4f 100644 --- a/src/cli/parser/cli_grammar.gd.uid +++ b/src/cli/parser/cli_grammar.gd.uid @@ -1 +1,2 @@ -uid://dx3jpmf8576om +uid://dwouisp5h8730 + diff --git a/src/cli/parser/cli_parser.gd.uid b/src/cli/parser/cli_parser.gd.uid index 3d61d256..ba22300d 100644 --- a/src/cli/parser/cli_parser.gd.uid +++ b/src/cli/parser/cli_parser.gd.uid @@ -1 +1,2 @@ -uid://drv2f0c0l4u0d +uid://djgkgl4ioox2a + diff --git a/src/cli/root_routes.gd.uid b/src/cli/root_routes.gd.uid index 6233582b..69613630 100644 --- a/src/cli/root_routes.gd.uid +++ b/src/cli/root_routes.gd.uid @@ -1 +1,2 @@ -uid://cj68wxb5oeirw +uid://m7d46xpwkylu + diff --git a/src/cli/routes/routes.gd.uid b/src/cli/routes/routes.gd.uid index 09d8673f..7dae0d42 100644 --- a/src/cli/routes/routes.gd.uid +++ b/src/cli/routes/routes.gd.uid @@ -1 +1,2 @@ -uid://bu7w8llxyhl01 +uid://cojccio0bkhp7 + diff --git a/src/components/actions_sidebar/actions_sidebar.gd.uid b/src/components/actions_sidebar/actions_sidebar.gd.uid index 3fa0ac6e..a51b5c06 100644 --- a/src/components/actions_sidebar/actions_sidebar.gd.uid +++ b/src/components/actions_sidebar/actions_sidebar.gd.uid @@ -1 +1,2 @@ -uid://buemprlv2220o +uid://cs25fwi1eiq52 + diff --git a/src/components/asset_download/asset_download.gd.uid b/src/components/asset_download/asset_download.gd.uid index 4ed3924e..10a74a9d 100644 --- a/src/components/asset_download/asset_download.gd.uid +++ b/src/components/asset_download/asset_download.gd.uid @@ -1 +1,2 @@ -uid://c0i8yg232fxrf +uid://b5yjhxlso16xw + diff --git a/src/components/asset_lib_projects/version_option_button.gd.uid b/src/components/asset_lib_projects/version_option_button.gd.uid index dae67160..17886bf9 100644 --- a/src/components/asset_lib_projects/version_option_button.gd.uid +++ b/src/components/asset_lib_projects/version_option_button.gd.uid @@ -1 +1 @@ -uid://dcjieukjjp1kc +uid://6scpn7l4t0gg diff --git a/src/components/command_viewer/command_text_view.gd.uid b/src/components/command_viewer/command_text_view.gd.uid index 33442add..d7ec1cb0 100644 --- a/src/components/command_viewer/command_text_view.gd.uid +++ b/src/components/command_viewer/command_text_view.gd.uid @@ -1 +1 @@ -uid://b563imc614q88 +uid://j62emu6knxl8 diff --git a/src/components/command_viewer/command_viewer.gd.uid b/src/components/command_viewer/command_viewer.gd.uid index 6946d9fc..4ba6139d 100644 --- a/src/components/command_viewer/command_viewer.gd.uid +++ b/src/components/command_viewer/command_viewer.gd.uid @@ -1 +1 @@ -uid://drl4ao34qu5vl +uid://ct0lj5nn7d0kb diff --git a/src/components/command_viewer/new_command_dialog.gd.uid b/src/components/command_viewer/new_command_dialog.gd.uid index e4611763..03d9b557 100644 --- a/src/components/command_viewer/new_command_dialog.gd.uid +++ b/src/components/command_viewer/new_command_dialog.gd.uid @@ -1 +1 @@ -uid://bdi4088olx6bk +uid://b8t1fub206xj3 diff --git a/src/components/downloads_container/downloads_container.gd.uid b/src/components/downloads_container/downloads_container.gd.uid index 3556638f..e31548ac 100644 --- a/src/components/downloads_container/downloads_container.gd.uid +++ b/src/components/downloads_container/downloads_container.gd.uid @@ -1 +1 @@ -uid://w12ykfbn1qty +uid://diqhx10q6a1ui diff --git a/src/components/editors/local/editor_import/editor_import.gd.uid b/src/components/editors/local/editor_import/editor_import.gd.uid index 620b469d..7d379da1 100644 --- a/src/components/editors/local/editor_import/editor_import.gd.uid +++ b/src/components/editors/local/editor_import/editor_import.gd.uid @@ -1 +1 @@ -uid://dsmk435txsdi4 +uid://7ew1luyl5fq diff --git a/src/components/editors/local/editor_item/add_extra_arguments_editor_dialog.gd.uid b/src/components/editors/local/editor_item/add_extra_arguments_editor_dialog.gd.uid index bf5450f0..fcf53fda 100644 --- a/src/components/editors/local/editor_item/add_extra_arguments_editor_dialog.gd.uid +++ b/src/components/editors/local/editor_item/add_extra_arguments_editor_dialog.gd.uid @@ -1 +1 @@ -uid://bq46f2ucocsue +uid://d1dd1xgjr8lq6 diff --git a/src/components/editors/local/editor_item/editor_item.gd.uid b/src/components/editors/local/editor_item/editor_item.gd.uid index f6ef199b..588fdaf0 100644 --- a/src/components/editors/local/editor_item/editor_item.gd.uid +++ b/src/components/editors/local/editor_item/editor_item.gd.uid @@ -1 +1 @@ -uid://bj3m1ptybcvdv +uid://cxpg6cwbxwga8 diff --git a/src/components/editors/local/editor_item/editor_item_actions.gd.uid b/src/components/editors/local/editor_item/editor_item_actions.gd.uid index dfb12068..00afbf78 100644 --- a/src/components/editors/local/editor_item/editor_item_actions.gd.uid +++ b/src/components/editors/local/editor_item/editor_item_actions.gd.uid @@ -1 +1 @@ -uid://cqf3e4d66giby +uid://cmxwodiq7hqd1 diff --git a/src/components/editors/local/editor_item/rename_editor_dialog.gd.uid b/src/components/editors/local/editor_item/rename_editor_dialog.gd.uid index e8d137fb..263951cf 100644 --- a/src/components/editors/local/editor_item/rename_editor_dialog.gd.uid +++ b/src/components/editors/local/editor_item/rename_editor_dialog.gd.uid @@ -1 +1 @@ -uid://dicmyggjqgwhh +uid://ewprwq3wupbc diff --git a/src/components/editors/local/editor_item/show_owners_dialog.gd.uid b/src/components/editors/local/editor_item/show_owners_dialog.gd.uid index 5b7da553..b555fe1b 100644 --- a/src/components/editors/local/editor_item/show_owners_dialog.gd.uid +++ b/src/components/editors/local/editor_item/show_owners_dialog.gd.uid @@ -1 +1 @@ -uid://owiu72mto8lo +uid://b0ycegltr46gd diff --git a/src/components/editors/local/editors_list/editors_list.gd.uid b/src/components/editors/local/editors_list/editors_list.gd.uid index 1d19d5c5..0fef0d28 100644 --- a/src/components/editors/local/editors_list/editors_list.gd.uid +++ b/src/components/editors/local/editors_list/editors_list.gd.uid @@ -1 +1 @@ -uid://lh4jgxu5iuij +uid://cytv2g64gkcoc diff --git a/src/components/editors/local/local_editors.gd.uid b/src/components/editors/local/local_editors.gd.uid index 2f859124..b1f9a4ee 100644 --- a/src/components/editors/local/local_editors.gd.uid +++ b/src/components/editors/local/local_editors.gd.uid @@ -1 +1 @@ -uid://d2ipnf5f4u4dx +uid://chfvicifd18o5 diff --git a/src/components/editors/local/orphan_editor_explorer/orphan_editor_explorer.gd.uid b/src/components/editors/local/orphan_editor_explorer/orphan_editor_explorer.gd.uid index b0b1ea14..8ca84d53 100644 --- a/src/components/editors/local/orphan_editor_explorer/orphan_editor_explorer.gd.uid +++ b/src/components/editors/local/orphan_editor_explorer/orphan_editor_explorer.gd.uid @@ -1 +1 @@ -uid://crkhiskhtfky1 +uid://c1qij1xf2smku diff --git a/src/components/editors/local_remote_editors_switch.gd.uid b/src/components/editors/local_remote_editors_switch.gd.uid index 994aaca5..c6f5d2d4 100644 --- a/src/components/editors/local_remote_editors_switch.gd.uid +++ b/src/components/editors/local_remote_editors_switch.gd.uid @@ -1 +1 @@ -uid://cvaj4y2hm7v8y +uid://h2gnkydq08rr diff --git a/src/components/editors/remote/remote_editor_direct_link/remote_editor_direct_link.tscn b/src/components/editors/remote/remote_editor_direct_link/remote_editor_direct_link.tscn index ffea4d0d..cb2a1bcb 100644 --- a/src/components/editors/remote/remote_editor_direct_link/remote_editor_direct_link.tscn +++ b/src/components/editors/remote/remote_editor_direct_link/remote_editor_direct_link.tscn @@ -1,34 +1,35 @@ -[gd_scene load_steps=2 format=3 uid="uid://dhvesrdvhm6lv"] +[gd_scene format=3 uid="uid://dhvesrdvhm6lv"] [ext_resource type="Script" uid="uid://xksdiv3vlx17" path="res://src/components/editors/remote/remote_editor_direct_link/remote_editor_direct_link.gd" id="1_nwbsu"] -[node name="RemoteEditorDirectLink" type="ConfirmationDialog"] +[node name="RemoteEditorDirectLink" type="ConfirmationDialog" unique_id=602156080] +oversampling_override = 1.0 title = "Enter Link" position = Vector2i(0, 36) size = Vector2i(557, 100) visible = true script = ExtResource("1_nwbsu") -[node name="VBoxContainer" type="VBoxContainer" parent="."] +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=150849877] offset_left = 8.0 offset_top = 8.0 offset_right = 549.0 offset_bottom = 51.0 -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" unique_id=1944310026] layout_mode = 2 -[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"] +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=1570362618] layout_mode = 2 text = "Link:" -[node name="UrlEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer"] +[node name="UrlEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer" unique_id=1859611064] unique_name_in_owner = true custom_minimum_size = Vector2(500, 0) layout_mode = 2 size_flags_horizontal = 3 placeholder_text = "https://" -[node name="Control" type="Control" parent="VBoxContainer"] +[node name="Control" type="Control" parent="VBoxContainer" unique_id=1066847016] layout_mode = 2 size_flags_vertical = 3 diff --git a/src/components/misc/item_tag_container.gd.uid b/src/components/misc/item_tag_container.gd.uid index a9774077..4eb1bccb 100644 --- a/src/components/misc/item_tag_container.gd.uid +++ b/src/components/misc/item_tag_container.gd.uid @@ -1 +1,2 @@ -uid://cq0ugsh475bgb +uid://crv1kad7iho35 + diff --git a/src/components/misc/list_item_icon.gd.uid b/src/components/misc/list_item_icon.gd.uid index e0fe92f5..be454257 100644 --- a/src/components/misc/list_item_icon.gd.uid +++ b/src/components/misc/list_item_icon.gd.uid @@ -1 +1,2 @@ -uid://bovwkd7qejtto +uid://dqsph2gdq1pcy + diff --git a/src/components/misc/list_item_path_label.gd.uid b/src/components/misc/list_item_path_label.gd.uid index 9bef2788..ea4a7279 100644 --- a/src/components/misc/list_item_path_label.gd.uid +++ b/src/components/misc/list_item_path_label.gd.uid @@ -1 +1,2 @@ -uid://bfylexg1c3dlf +uid://b2f07teiqjwks + diff --git a/src/components/misc/list_item_title_label.gd.uid b/src/components/misc/list_item_title_label.gd.uid index 29c38c1a..01812880 100644 --- a/src/components/misc/list_item_title_label.gd.uid +++ b/src/components/misc/list_item_title_label.gd.uid @@ -1 +1,2 @@ -uid://kre8bx3enixn +uid://b3ufqis2fwjja + diff --git a/src/components/misc/news_button.gd.uid b/src/components/misc/news_button.gd.uid index 182c582b..46a4f8df 100644 --- a/src/components/misc/news_button.gd.uid +++ b/src/components/misc/news_button.gd.uid @@ -1 +1,2 @@ -uid://dcf4qjuio4jrj +uid://8elf35pbu73b + diff --git a/src/components/misc/notifications_button.gd.uid b/src/components/misc/notifications_button.gd.uid index c513b121..816ec645 100644 --- a/src/components/misc/notifications_button.gd.uid +++ b/src/components/misc/notifications_button.gd.uid @@ -1 +1,2 @@ -uid://cem5jcbasufp +uid://cunmpmglcvdsv + diff --git a/src/components/misc/remove_missing_dialog.gd.uid b/src/components/misc/remove_missing_dialog.gd.uid index d36cde04..8c9e1b67 100644 --- a/src/components/misc/remove_missing_dialog.gd.uid +++ b/src/components/misc/remove_missing_dialog.gd.uid @@ -1 +1,2 @@ -uid://c3c0pve1yay1i +uid://bidesw0jinqch + diff --git a/src/components/misc/scan_file_dialog.gd.uid b/src/components/misc/scan_file_dialog.gd.uid index dfccd3ad..c3f4b06d 100644 --- a/src/components/misc/scan_file_dialog.gd.uid +++ b/src/components/misc/scan_file_dialog.gd.uid @@ -1 +1,2 @@ -uid://c7t87f8ge7wlu +uid://dgiw7p70j4ng1 + diff --git a/src/components/misc/tab_actions.gd.uid b/src/components/misc/tab_actions.gd.uid index 19fed8a5..2271c239 100644 --- a/src/components/misc/tab_actions.gd.uid +++ b/src/components/misc/tab_actions.gd.uid @@ -1 +1,2 @@ -uid://c80dmhw2jwsa8 +uid://doc32tfge4kxi + diff --git a/src/components/misc/themed_button.gd.uid b/src/components/misc/themed_button.gd.uid index 97a8a901..e09587e4 100644 --- a/src/components/misc/themed_button.gd.uid +++ b/src/components/misc/themed_button.gd.uid @@ -1 +1,2 @@ -uid://t1bc02eqd4s4 +uid://c3y7le17wngkf + diff --git a/src/components/project_templates/open_template_dialog.gd b/src/components/project_templates/open_template_dialog.gd new file mode 100644 index 00000000..ba87e06d --- /dev/null +++ b/src/components/project_templates/open_template_dialog.gd @@ -0,0 +1,135 @@ +class_name OpenTemplateDialog +extends ConfirmationDialog + +signal opened(project_name: String, target_path: String, template_item: ProjectTemplates.Item) + +@onready var _project_name_edit: LineEdit = %ProjectNameEdit +@onready var _project_path_edit: LineEdit = %ProjectPathLineEdit +@onready var _browse_path_button: Button = %BrowsePathButton +@onready var _create_folder_check: CheckButton = %CreateFolderCheck +@onready var _message_label: Label = %MessageLabel +@onready var _status_rect: TextureRect = %StatusRect +@onready var _file_dialog: FileDialog = $FileDialog + +var _template_item: ProjectTemplates.Item +var _auto_dir: String = "" + + +func _ready() -> void: + dialog_hide_on_ok = false + _browse_path_button.icon = get_theme_icon("Load", "EditorIcons") + + _project_name_edit.text_changed.connect(func(_arg: String) -> void: + _update_project_dir() + _validate() + ) + _project_path_edit.text_changed.connect(func(_arg: String) -> void: _validate()) + + _browse_path_button.pressed.connect(func() -> void: + var path := _project_path_edit.text.strip_edges() + if _create_folder_check.button_pressed: + _file_dialog.current_dir = path.get_base_dir() + else: + _file_dialog.current_dir = path + _file_dialog.popup_centered_ratio(0.5) + ) + + _file_dialog.dir_selected.connect(func(dir: String) -> void: + if _create_folder_check.button_pressed: + var folder := _project_path_edit.text.get_file() + if folder != _auto_dir: + folder = _auto_dir + _project_path_edit.text = dir.path_join(folder) + else: + _project_path_edit.text = dir + _validate() + ) + + _create_folder_check.toggled.connect(func(pressed: bool) -> void: + var path := _project_path_edit.text + if pressed: + path = path.path_join(_auto_dir) + else: + path = path.rstrip("/\\") + if path.get_file() == _auto_dir: + pass + else: + pass + path = path.get_base_dir() + _project_path_edit.text = path + _validate() + ) + + confirmed.connect(func() -> void: + var project_name := _project_name_edit.text.strip_edges() + var target_path := _project_path_edit.text.strip_edges() + + if _create_folder_check.button_pressed: + DirAccess.make_dir_recursive_absolute(target_path) + + opened.emit(project_name, target_path, _template_item) + hide() + ) + + min_size = Vector2(640, 200) * Config.EDSCALE + + +func raise(template_item: ProjectTemplates.Item) -> void: + _template_item = template_item + _project_name_edit.text = template_item.name + _project_path_edit.text = Config.DEFAULT_PROJECTS_PATH.ret() + _auto_dir = "" + _validate() + popup_centered() + _project_name_edit.grab_focus() + _project_name_edit.select_all() + _update_project_dir() + + +func _update_project_dir() -> void: + var project_name := _project_name_edit.text.strip_edges() + var new_auto_dir := project_name.to_snake_case().validate_filename() + if _create_folder_check.button_pressed: + var path := _project_path_edit.text + if path.get_file() == _auto_dir or _auto_dir.is_empty(): + _project_path_edit.text = path.get_base_dir().path_join(new_auto_dir) + _auto_dir = new_auto_dir + _validate() + + +func _validate() -> void: + var project_name := _project_name_edit.text.strip_edges() + var path := _project_path_edit.text.strip_edges() + + if project_name.is_empty(): + _error(tr("Project name cannot be blank.")) + return + + if path.is_empty(): + _error(tr("Target path cannot be blank.")) + return + + if _create_folder_check.button_pressed: + _success(tr("The project folder will be automatically created.")) + return + + var dir := DirAccess.open(path) + if not dir: + _error(tr("The path specified doesn't exist.")) + return + + _success(tr("Ready to create project from template.")) + + +func _error(text: String) -> void: + _message_label.text = text + _message_label.add_theme_color_override("font_color", get_theme_color("error_color", "Editor")) + _status_rect.texture = get_theme_icon("StatusError", "EditorIcons") + get_ok_button().disabled = true + + +func _success(text: String) -> void: + _message_label.text = text + _message_label.add_theme_color_override("font_color", get_theme_color("success_color", "Editor")) + _status_rect.texture = get_theme_icon("StatusSuccess", "EditorIcons") + get_ok_button().disabled = false diff --git a/src/components/project_templates/open_template_dialog.tscn b/src/components/project_templates/open_template_dialog.tscn new file mode 100644 index 00000000..03a9c05e --- /dev/null +++ b/src/components/project_templates/open_template_dialog.tscn @@ -0,0 +1,65 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://src/components/project_templates/open_template_dialog.gd" id="1_script"] + +[node name="OpenTemplateDialog" type="ConfirmationDialog"] +title = "Create Project from Template" +ok_button_text = "Create" +script = ExtResource("1_script") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="NameLabel" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Project Name:" + +[node name="ProjectNameEdit" type="LineEdit" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "My New Project" + +[node name="PathLabel" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Target Path:" + +[node name="PathHBox" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="ProjectPathLineEdit" type="LineEdit" parent="VBoxContainer/PathHBox"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="BrowsePathButton" type="Button" parent="VBoxContainer/PathHBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "Browse" + +[node name="CreateFolderCheck" type="CheckButton" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Create project folder" + +[node name="MessageHBox" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="StatusRect" type="TextureRect" parent="VBoxContainer/MessageHBox"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(16, 16) +stretch_mode = 5 + +[node name="MessageLabel" type="Label" parent="VBoxContainer/MessageHBox"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "" + +[node name="FileDialog" type="FileDialog" parent="."] +title = "Select Target Directory" +ok_button_text = "Select" +file_mode = 2 +access = 2 diff --git a/src/components/project_templates/project_templates.gd b/src/components/project_templates/project_templates.gd new file mode 100644 index 00000000..f5a79e8c --- /dev/null +++ b/src/components/project_templates/project_templates.gd @@ -0,0 +1,194 @@ +class_name ProjectTemplatesControl +extends HBoxContainer + +@onready var _sidebar: ActionsSidebarControl = %ActionsSidebar +@onready var _templates_list: ProjectTemplatesVBoxList = %ProjectTemplatesList +@onready var _save_template_dialog: SaveTemplateDialog = %SaveTemplateDialog +@onready var _open_template_dialog: OpenTemplateDialog = %OpenTemplateDialog + +var _templates: ProjectTemplates.List +var _projects_service: Projects.List + + +func init(templates: ProjectTemplates.List, projects_service: Projects.List) -> void: + _templates = templates + _projects_service = projects_service + + var actions := Action.List.new([ + Action.from_dict({ + "key": "save-template", + "icon": Action.IconTheme.new(self, "Save", "EditorIcons"), + "act": func() -> void: _save_template_dialog.raise(), + "label": tr("Save as Template"), + }), + Action.from_dict({ + "key": "import-template", + "icon": Action.IconTheme.new(self, "Load", "EditorIcons"), + "act": _import_template, + "label": tr("Import Template"), + }), + Action.from_dict({ + "key": "refresh", + "icon": Action.IconTheme.new(self, "Reload", "EditorIcons"), + "act": _refresh, + "label": tr("Refresh List"), + }) + ]) + + $ProjectTemplatesList/HBoxContainer.add_child(actions.by_key('refresh').to_btn().make_flat(true).show_text(false)) + $ProjectTemplatesList/HBoxContainer.add_child(actions.by_key('save-template').to_btn().make_flat(true).show_text(false)) + $ProjectTemplatesList/HBoxContainer.add_child(actions.by_key('import-template').to_btn().make_flat(true).show_text(false)) + + _save_template_dialog.saved.connect(_on_save_template) + _open_template_dialog.opened.connect(_on_open_template) + + _templates_list.refresh(_templates.all()) + _load_templates() + + +func _load_templates() -> void: + for template: ProjectTemplates.Item in _templates.all(): + template.loaded.connect(func() -> void: + _templates_list.sort_items() + ) + _templates_list.sort_items() + _templates_list.update_filters() + + +func _refresh() -> void: + _templates.load() + _templates_list.refresh(_templates.all()) + _load_templates() + + +func _on_save_template(template_name: String, description: String, source_path: String, tags: Array) -> void: + var template := _templates.add(template_name, description, source_path, tags) + _templates.save() + + # Create the zip from the source project + var project_dir := source_path.get_base_dir() + var err := _create_template_zip(project_dir, template.zip_path) + if err != OK: + Output.push("Failed to create template zip: %s" % err) + return + + _templates_list.add(template) + _templates_list.sort_items() + + +func _create_template_zip(source_dir: String, target_zip_path: String) -> Error: + # Use zip command to create archive from source directory + var output := [] + var exit_code: int + + # Ensure target directory exists + DirAccess.make_dir_recursive_absolute(target_zip_path.get_base_dir()) + + # Remove existing zip if present + if FileAccess.file_exists(target_zip_path): + DirAccess.remove_absolute(target_zip_path) + + if OS.has_feature("windows"): + exit_code = OS.execute( + "powershell.exe", + [ + "-command", + "Set-Location '%s'; Compress-Archive -Path '*' -DestinationPath '%s' -Force" % [ + source_dir, + ProjectSettings.globalize_path(target_zip_path) + ] + ], output, true + ) + else: + exit_code = OS.execute( + "bash", + [ + "-c", + "cd '%s' && zip -r '%s' ." % [ + source_dir, + ProjectSettings.globalize_path(target_zip_path) + ] + ], output, true + ) + + Output.push(output.pop_front()) + Output.push("Template zip created with exit code: %s" % exit_code) + return OK if exit_code == 0 else FAILED + + +func _on_open_template(project_name: String, target_path: String, template_item: ProjectTemplates.Item) -> void: + if not template_item.is_zip_valid: + Output.push("Template zip file is missing: %s" % template_item.zip_path) + return + + # Unzip the template to the target path + var zip_reader := ZIPReader.new() + var unzip_err := zip_reader.open(template_item.zip_path) + if unzip_err != OK: + zip_reader.close() + Output.push("Failed to open template zip: %s" % unzip_err) + return + + var unzip_result := zip.unzip_to_path(zip_reader, target_path) + zip_reader.close() + + if unzip_result != OK: + Output.push("Failed to extract template: %s" % unzip_result) + return + + # Find project.godot in the extracted files + var project_configs := utils.find_project_godot_files(target_path) + if len(project_configs) == 0: + Output.push("No project.godot found in extracted template") + return + + # Update the project name in project.godot + var project_file_path := project_configs[0].path + var cfg := ConfigFile.new() + var err := cfg.load(project_file_path) + if not err: + cfg.set_value("application", "config/name", project_name) + cfg.save(project_file_path) + + # Import the project into the projects list + var project := _projects_service.add(project_file_path, "") + project.load() + _projects_service.save() + + _templates_list.sort_items() + + +func _import_template() -> void: + # Import a template from a zip file + var file_dialog := FileDialog.new() + file_dialog.title = "Import Template Zip" + file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE + file_dialog.access = FileDialog.ACCESS_FILESYSTEM + file_dialog.filters = PackedStringArray(["*.zip ; Zip Files"]) + + file_dialog.file_selected.connect(func(path: String) -> void: + var template_name := path.get_file().replace(".zip", "").capitalize() + var template := _templates.add(template_name, "Imported template", "", []) + _templates.save() + + # Copy the zip file to the templates directory + var err := DirAccess.copy_absolute(path, template.zip_path) + if err != OK: + Output.push("Failed to copy template zip: %s" % err) + return + + _templates_list.add(template) + _templates_list.sort_items() + ) + + add_child(file_dialog) + file_dialog.popup_centered_ratio(0.5) + + +func _on_templates_list_item_selected(item: TemplateListItemControl) -> void: + pass + + +func _on_templates_list_item_removed(item_data: ProjectTemplates.Item) -> void: + _templates.erase(item_data.id) + _templates.save() diff --git a/src/components/project_templates/project_templates.tscn b/src/components/project_templates/project_templates.tscn new file mode 100644 index 00000000..1f04baa8 --- /dev/null +++ b/src/components/project_templates/project_templates.tscn @@ -0,0 +1,42 @@ +[gd_scene load_steps=7 format=3] + +[ext_resource type="PackedScene" uid="uid://cuuiumge42ghh" path="res://src/components/actions_sidebar/actions_sidebar.tscn" id="1_sidebar"] +[ext_resource type="PackedScene" path="res://src/components/project_templates/project_templates_list.tscn" id="2_list"] +[ext_resource type="Script" path="res://src/components/project_templates/project_templates.gd" id="3_script"] +[ext_resource type="PackedScene" path="res://src/components/project_templates/save_template_dialog.tscn" id="4_save"] +[ext_resource type="PackedScene" path="res://src/components/project_templates/open_template_dialog.tscn" id="5_open"] + +[node name="ProjectTemplates" type="HBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3_script") + +[node name="ProjectTemplatesList" parent="." instance=ExtResource("2_list")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +_search_cache_is_enabled = true +_cached_search_key = "templates" + +[node name="VBoxContainer" parent="ProjectTemplatesList/HBoxContainer2" index="1"] +visible = false + +[node name="ActionsSidebar" parent="ProjectTemplatesList/HBoxContainer2/VBoxContainer/SidebarContainer" index="0" instance=ExtResource("1_sidebar")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="SaveTemplateDialog" parent="." instance=ExtResource("4_save")] +unique_name_in_owner = true +visible = false + +[node name="OpenTemplateDialog" parent="." instance=ExtResource("5_open")] +unique_name_in_owner = true +visible = false + +[connection signal="item_opened" from="ProjectTemplatesList" to="." method="_on_templates_list_item_selected"] + +[editable path="ProjectTemplatesList"] +[editable path="ProjectTemplatesList/HBoxContainer2/VBoxContainer/SidebarContainer/ActionsSidebar"] diff --git a/src/components/project_templates/project_templates_list.gd b/src/components/project_templates/project_templates_list.gd new file mode 100644 index 00000000..4b6f2139 --- /dev/null +++ b/src/components/project_templates/project_templates_list.gd @@ -0,0 +1,29 @@ +class_name ProjectTemplatesVBoxList +extends VBoxList + +signal item_opened(item_data: ProjectTemplates.Item) +signal item_removed(item_data: ProjectTemplates.Item) + + +func _post_add(raw_item_data: Object, raw_item_control: Control) -> void: + var item_data := raw_item_data as ProjectTemplates.Item + var item_control := raw_item_control as TemplateListItemControl + item_control.opened.connect( + func() -> void: item_opened.emit(item_data) + ) + + +func _item_comparator(a: Dictionary, b: Dictionary) -> bool: + match _sort_option_button.selected: + 0: return a.created_at > b.created_at + _: return a.name < b.name + return a.name < b.name + + +func _fill_sort_options(btn: OptionButton) -> void: + btn.add_item(tr("Newest First")) + btn.add_item(tr("Name")) + + var last_checked_sort := Cache.smart_value(self, "last_checked_sort", true) + btn.select(last_checked_sort.ret(0) as int) + btn.item_selected.connect(func(idx: int) -> void: last_checked_sort.put(idx)) diff --git a/src/components/project_templates/project_templates_list.tscn b/src/components/project_templates/project_templates_list.tscn new file mode 100644 index 00000000..49d1cdf5 --- /dev/null +++ b/src/components/project_templates/project_templates_list.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="PackedScene" uid="uid://bjudiph2xbmbu" path="res://src/components/v_box_list/v_box_list.tscn" id="1_base"] +[ext_resource type="Script" path="res://src/components/project_templates/project_templates_list.gd" id="2_list"] +[ext_resource type="PackedScene" path="res://src/components/project_templates/template_list_item.tscn" id="3_item"] + +[node name="ProjectTemplatesList" instance=ExtResource("1_base")] +script = ExtResource("2_list") +_item_scene = ExtResource("3_item") + +[node name="SearchBox" parent="HBoxContainer" index="1"] +placeholder_text = "Filter Templates" + +[node name="Label" parent="HBoxContainer/HBoxContainer" index="0"] +size_flags_vertical = 1 +horizontal_alignment = 1 +vertical_alignment = 1 diff --git a/src/components/project_templates/save_template_dialog.gd b/src/components/project_templates/save_template_dialog.gd new file mode 100644 index 00000000..50d68792 --- /dev/null +++ b/src/components/project_templates/save_template_dialog.gd @@ -0,0 +1,83 @@ +class_name SaveTemplateDialog +extends ConfirmationDialog + +signal saved(template_name: String, description: String, source_path: String, tags: Array) + +@onready var _template_name_edit: LineEdit = %TemplateNameEdit +@onready var _description_edit: TextEdit = %DescriptionEdit +@onready var _source_path_edit: LineEdit = %SourcePathEdit +@onready var _browse_source_button: Button = %BrowseSourceButton +@onready var _message_label: Label = %MessageLabel +@onready var _status_rect: TextureRect = %StatusRect +@onready var _file_dialog: FileDialog = $FileDialog + + +func _ready() -> void: + dialog_hide_on_ok = false + _browse_source_button.icon = get_theme_icon("Load", "EditorIcons") + + _source_path_edit.text_changed.connect(func(_arg: String) -> void: _validate()) + _template_name_edit.text_changed.connect(func(_arg: String) -> void: _validate()) + + _browse_source_button.pressed.connect(func() -> void: + _file_dialog.current_dir = _source_path_edit.text.strip_edges() + _file_dialog.popup_centered_ratio(0.5) + ) + + _file_dialog.dir_selected.connect(func(dir: String) -> void: + _source_path_edit.text = dir + _validate() + ) + + confirmed.connect(func() -> void: + var template_name := _template_name_edit.text.strip_edges() + var description := _description_edit.text.strip_edges() + var source_path := _source_path_edit.text.strip_edges() + saved.emit(template_name, description, source_path, []) + hide() + ) + + min_size = Vector2(640, 300) * Config.EDSCALE + + +func raise(source_project_path: String = "") -> void: + _template_name_edit.text = "" + _description_edit.text = "" + _source_path_edit.text = source_project_path + _validate() + popup_centered() + _template_name_edit.grab_focus() + + +func _validate() -> void: + var template_name := _template_name_edit.text.strip_edges() + var source_path := _source_path_edit.text.strip_edges() + + if template_name.is_empty(): + _error(tr("Template name cannot be blank.")) + return + + if source_path.is_empty(): + _error(tr("Source project path cannot be blank.")) + return + + var project_file := source_path.path_join("project.godot") + if not FileAccess.file_exists(project_file): + _error(tr("The selected path does not contain a project.godot file.")) + return + + _success(tr("Ready to save template.")) + + +func _error(text: String) -> void: + _message_label.text = text + _message_label.add_theme_color_override("font_color", get_theme_color("error_color", "Editor")) + _status_rect.texture = get_theme_icon("StatusError", "EditorIcons") + get_ok_button().disabled = true + + +func _success(text: String) -> void: + _message_label.text = text + _message_label.add_theme_color_override("font_color", get_theme_color("success_color", "Editor")) + _status_rect.texture = get_theme_icon("StatusSuccess", "EditorIcons") + get_ok_button().disabled = false diff --git a/src/components/project_templates/save_template_dialog.tscn b/src/components/project_templates/save_template_dialog.tscn new file mode 100644 index 00000000..d942930f --- /dev/null +++ b/src/components/project_templates/save_template_dialog.tscn @@ -0,0 +1,72 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://src/components/project_templates/save_template_dialog.gd" id="1_script"] + +[node name="SaveTemplateDialog" type="ConfirmationDialog"] +title = "Save Project as Template" +ok_button_text = "Save" +script = ExtResource("1_script") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="NameLabel" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Template Name:" + +[node name="TemplateNameEdit" type="LineEdit" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "My Awesome Template" + +[node name="DescLabel" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Description:" + +[node name="DescriptionEdit" type="TextEdit" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +custom_minimum_size = Vector2(0, 80) +placeholder_text = "Describe what this template includes..." + +[node name="SourceLabel" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Source Project:" + +[node name="SourceHBox" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="SourcePathEdit" type="LineEdit" parent="VBoxContainer/SourceHBox"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +editable = false + +[node name="BrowseSourceButton" type="Button" parent="VBoxContainer/SourceHBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "Browse" + +[node name="MessageHBox" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="StatusRect" type="TextureRect" parent="VBoxContainer/MessageHBox"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(16, 16) +stretch_mode = 5 + +[node name="MessageLabel" type="Label" parent="VBoxContainer/MessageHBox"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "" + +[node name="FileDialog" type="FileDialog" parent="."] +title = "Select Project Directory" +ok_button_text = "Select" +file_mode = 2 +access = 2 diff --git a/src/components/project_templates/template_list_item.gd b/src/components/project_templates/template_list_item.gd new file mode 100644 index 00000000..9ad43a0a --- /dev/null +++ b/src/components/project_templates/template_list_item.gd @@ -0,0 +1,62 @@ +class_name TemplateListItemControl +extends HBoxListItem + +signal opened +signal removed +signal tag_clicked(tag: String) + +@onready var _title_label: Label = %TitleLabel +@onready var _description_label: Label = %DescriptionLabel +@onready var _date_label: Label = %DateLabel +@onready var _tag_container: ItemTagContainer = %TagContainer +@onready var _icon: TextureRect = $Icon +@onready var _open_button: Button = %OpenButton +@onready var _actions_container: HBoxContainer = %ActionsContainer + +var _template: ProjectTemplates.Item +var _tags := [] +var _sort_data := { + 'ref': self +} + + +func _ready() -> void: + super._ready() + _tag_container.tag_clicked.connect(func(tag: String) -> void: tag_clicked.emit(tag)) + + +func init(item: ProjectTemplates.Item) -> void: + _template = item + _fill_data(item) + + _open_button.pressed.connect(func() -> void: opened.emit()) + + double_clicked.connect(func() -> void: opened.emit()) + + +func _fill_data(item: ProjectTemplates.Item) -> void: + _title_label.text = item.name + _description_label.text = item.description if not item.description.is_empty() else tr("No description") + _date_label.text = item.get_formatted_date() + _icon.texture = get_theme_icon("FileList", "EditorIcons") + _tag_container.set_tags(item.tags) + _tags = item.tags + + if not item.is_zip_valid: + modulate = Color(1, 1, 1, 0.498) + + _sort_data.name = item.name + _sort_data.created_at = item.created_at + _sort_data.tag_sort_string = "".join(item.tags) + + +func apply_filter(filter: Callable) -> bool: + return filter.call({ + 'name': _title_label.text, + 'path': '', + 'tags': _tags + }) + + +func get_sort_data() -> Dictionary: + return _sort_data diff --git a/src/components/project_templates/template_list_item.tscn b/src/components/project_templates/template_list_item.tscn new file mode 100644 index 00000000..5ae553b5 --- /dev/null +++ b/src/components/project_templates/template_list_item.tscn @@ -0,0 +1,73 @@ +[gd_scene load_steps=7 format=3] + +[ext_resource type="Script" path="res://src/components/project_templates/template_list_item.gd" id="1_script"] +[ext_resource type="Script" path="res://src/components/misc/list_item_title_label.gd" id="2_title"] +[ext_resource type="Script" path="res://src/components/misc/list_item_icon.gd" id="3_icon"] +[ext_resource type="Script" path="res://src/components/misc/item_tag_container.gd" id="4_tags"] +[ext_resource type="Script" path="res://src/components/misc/list_item_path_label.gd" id="5_path"] +[ext_resource type="Script" path="res://src/components/misc/themed_button.gd" id="6_btn"] + +[node name="TemplateListItem" type="HBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_script") + +[node name="Icon" type="TextureRect" parent="."] +layout_mode = 2 +script = ExtResource("3_icon") + +[node name="InfoVBox" type="VBoxContainer" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 2 + +[node name="TitleContainer" type="HBoxContainer" parent="InfoVBox"] +layout_mode = 2 + +[node name="TitleLabel" type="Label" parent="InfoVBox/TitleContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Template Name" +text_overrun_behavior = 3 +script = ExtResource("2_title") + +[node name="DateLabel" type="Label" parent="InfoVBox/TitleContainer"] +unique_name_in_owner = true +layout_mode = 2 +modulate = Color(1, 1, 1, 0.498039) +text = "2025-01-01" +script = ExtResource("5_path") + +[node name="ActionsContainer" type="HBoxContainer" parent="InfoVBox/TitleContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="OpenButton" type="Button" parent="InfoVBox/TitleContainer/ActionsContainer"] +unique_name_in_owner = true +layout_mode = 2 +flat = true +script = ExtResource("6_btn") + +[node name="DescriptionContainer" type="HBoxContainer" parent="InfoVBox"] +layout_mode = 2 + +[node name="DescriptionLabel" type="Label" parent="InfoVBox/DescriptionContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Template description" +text_overrun_behavior = 3 +autowrap_mode = 2 + +[node name="TagsContainer" type="HBoxContainer" parent="InfoVBox"] +layout_mode = 2 + +[node name="TagContainer" type="HBoxContainer" parent="InfoVBox/TagsContainer"] +unique_name_in_owner = true +layout_mode = 2 +script = ExtResource("4_tags") diff --git a/src/components/projects/clone_project_dialog/clone_project_dialog.gd.uid b/src/components/projects/clone_project_dialog/clone_project_dialog.gd.uid index f2273990..7ce47663 100644 --- a/src/components/projects/clone_project_dialog/clone_project_dialog.gd.uid +++ b/src/components/projects/clone_project_dialog/clone_project_dialog.gd.uid @@ -1 +1,2 @@ -uid://csivy81kvgyy6 +uid://c0rcoq0loj7cn + diff --git a/src/components/projects/duplicate_project_dialog/duplicate_project_dialog.gd.uid b/src/components/projects/duplicate_project_dialog/duplicate_project_dialog.gd.uid index 5c67cd1a..bf2e2853 100644 --- a/src/components/projects/duplicate_project_dialog/duplicate_project_dialog.gd.uid +++ b/src/components/projects/duplicate_project_dialog/duplicate_project_dialog.gd.uid @@ -1 +1,2 @@ -uid://uwkkt4y4y7yp +uid://x5sf8deuwf4o + diff --git a/src/components/projects/import_project_dialog/import_project_dialog.gd.uid b/src/components/projects/import_project_dialog/import_project_dialog.gd.uid index dcb1720d..5aef70eb 100644 --- a/src/components/projects/import_project_dialog/import_project_dialog.gd.uid +++ b/src/components/projects/import_project_dialog/import_project_dialog.gd.uid @@ -1 +1,2 @@ -uid://daqdfjktn6x2w +uid://bggavcdwbk1p7 + diff --git a/src/components/projects/install_project_dialog/install_project_dialog.gd.uid b/src/components/projects/install_project_dialog/install_project_dialog.gd.uid index 13086e13..25dfe697 100644 --- a/src/components/projects/install_project_dialog/install_project_dialog.gd.uid +++ b/src/components/projects/install_project_dialog/install_project_dialog.gd.uid @@ -1 +1,2 @@ -uid://bgksyramgob0s +uid://f4nffg2qcxw0 + diff --git a/src/components/projects/install_project_dialog/install_project_simple.gd.uid b/src/components/projects/install_project_dialog/install_project_simple.gd.uid index 0d2e4451..3a2012b0 100644 --- a/src/components/projects/install_project_dialog/install_project_simple.gd.uid +++ b/src/components/projects/install_project_dialog/install_project_simple.gd.uid @@ -1 +1,2 @@ -uid://8kh0gesqlera +uid://b357prgxhdex0 + diff --git a/src/components/projects/new_project_dialog/new_project_dialog.gd.uid b/src/components/projects/new_project_dialog/new_project_dialog.gd.uid index 7b14f03e..7ecb09c4 100644 --- a/src/components/projects/new_project_dialog/new_project_dialog.gd.uid +++ b/src/components/projects/new_project_dialog/new_project_dialog.gd.uid @@ -1 +1,2 @@ -uid://dw7y2kxdm4dqk +uid://dqpi63ys4sl5x + diff --git a/src/components/projects/project_item/project_item.gd b/src/components/projects/project_item/project_item.gd index 25ef8789..c7949212 100644 --- a/src/components/projects/project_item/project_item.gd +++ b/src/components/projects/project_item/project_item.gd @@ -202,6 +202,13 @@ func _fill_actions(item: Projects.Item) -> void: "label": tr("Show in File Manager"), }) + var save_as_template := Action.from_dict({ + "key": "save-as-template", + "icon": Action.IconTheme.new(self, "FileList", "EditorIcons"), + "act": _save_as_template.bind(item), + "label": tr("Save as Template"), + }) + _actions = Action.List.new([ edit, run, @@ -211,6 +218,7 @@ func _fill_actions(item: Projects.Item) -> void: manage_tags, view_command, show_in_file_manager, + save_as_template, remove ]) @@ -240,7 +248,8 @@ func _fill_data(item: Projects.Item) -> void: 'duplicate', 'bind-editor', 'manage-tags', - 'rename' + 'rename', + 'save-as-template' ]).all(): action.disable(item.is_missing) @@ -409,6 +418,86 @@ func _show_in_file_manager(item: Projects.Item) -> void: OS.shell_show_in_file_manager(ProjectSettings.globalize_path(item.path).get_base_dir()) +func _save_as_template(item: Projects.Item) -> void: + var templates_service := Context.use_or_null(self, ProjectTemplates.List) + if templates_service == null: + return + + # Create a simple confirmation dialog + var dialog := ConfirmationDialogAutoFree.new() + dialog.title = tr("Save as Template") + dialog.dialog_text = tr("Save '%s' as a project template?") % item.name + dialog.ok_button_text = tr("Save") + + var vbox := VBoxContainer.new() + dialog.add_child(vbox) + + var name_label := Label.new() + name_label.text = tr("Template Name:") + vbox.add_child(name_label) + + var name_edit := LineEdit.new() + name_edit.text = item.name + " Template" + vbox.add_child(name_edit) + + var desc_label := Label.new() + desc_label.text = tr("Description:") + vbox.add_child(desc_label) + + var desc_edit := TextEdit.new() + desc_edit.custom_minimum_size = Vector2(0, 60) * Config.EDSCALE + vbox.add_child(desc_edit) + + dialog.confirmed.connect(func() -> void: + var template_name := name_edit.text.strip_edges() + var description := desc_edit.text.strip_edges() + var source_path := ProjectSettings.globalize_path(item.path).get_base_dir() + + var template := templates_service.add(template_name, description, source_path, item.tags) + templates_service.save() + + # Create zip from project directory + _create_template_zip(source_path, template.zip_path) + ) + + add_child(dialog) + dialog.popup_centered(Vector2(400, 0) * Config.EDSCALE) + + +func _create_template_zip(source_dir: String, target_zip_path: String) -> void: + var output := [] + var exit_code: int + + DirAccess.make_dir_recursive_absolute(target_zip_path.get_base_dir()) + + if FileAccess.file_exists(target_zip_path): + DirAccess.remove_absolute(target_zip_path) + + # Use cd + zip to create archive from source directory + if OS.has_feature("windows"): + exit_code = OS.execute( + "powershell.exe", + [ + "-command", + "Set-Location '%s'; Compress-Archive -Path '*' -DestinationPath '%s' -Force" % [ + source_dir, + ProjectSettings.globalize_path(target_zip_path) + ] + ], output, true + ) + else: + exit_code = OS.execute( + "bash", + [ + "-c", + "cd '%s' && zip -r '%s' ." % [ + source_dir, + ProjectSettings.globalize_path(target_zip_path) + ] + ], output, true + ) + + func _run_with_editor(item: Projects.Item, editor_flag: Callable, auto_close: bool) -> void: editor_flag.call(item) diff --git a/src/components/projects/project_item/project_item.gd.uid b/src/components/projects/project_item/project_item.gd.uid index 51ed5d2f..50f6bdda 100644 --- a/src/components/projects/project_item/project_item.gd.uid +++ b/src/components/projects/project_item/project_item.gd.uid @@ -1 +1,2 @@ -uid://cwty5md5cdecc +uid://cbigewfqfrt1f + diff --git a/src/components/projects/project_item/project_item_actions.gd.uid b/src/components/projects/project_item/project_item_actions.gd.uid index 91b4cc28..f613945e 100644 --- a/src/components/projects/project_item/project_item_actions.gd.uid +++ b/src/components/projects/project_item/project_item_actions.gd.uid @@ -1 +1,2 @@ -uid://bocsjyk8w0a1e +uid://do7j82ew8psq + diff --git a/src/components/projects/projects.gd.uid b/src/components/projects/projects.gd.uid index 9a496893..9cf3c983 100644 --- a/src/components/projects/projects.gd.uid +++ b/src/components/projects/projects.gd.uid @@ -1 +1,2 @@ -uid://dh5y6638auqap +uid://dycjmdtkicrrs + diff --git a/src/components/projects/projects_list/projects_list.gd.uid b/src/components/projects/projects_list/projects_list.gd.uid index d7ad5858..e1386eec 100644 --- a/src/components/projects/projects_list/projects_list.gd.uid +++ b/src/components/projects/projects_list/projects_list.gd.uid @@ -1 +1,2 @@ -uid://dyqesthabburv +uid://b0dyq5oyub80j + diff --git a/src/components/rss_feed/list/rss_feed_item.gd b/src/components/rss_feed/list/rss_feed_item.gd new file mode 100644 index 00000000..c7c05aac --- /dev/null +++ b/src/components/rss_feed/list/rss_feed_item.gd @@ -0,0 +1,79 @@ +class_name RssFeedItemControl +extends HBoxListItem + +signal tag_clicked(tag: String) + +@onready var _title_label := %TitleLabel as Label +@onready var _source_label := %SourceLabel as Label +@onready var _date_label := %DateLabel as Label +@onready var _open_button := %OpenButton as Button +@onready var _thumbnail := %Thumbnail as TextureRect + +var _item: RssFeed.FeedItem +var _tags: Array = [] +var _images_src: RemoteImageSrc.I + + +const DEFAULT_ICON = preload("res://assets/default_project_icon.svg") + + +func init(item: RssFeed.FeedItem) -> void: + _item = item + _tags = [item.source_name] + + _title_label.text = item.title + _source_label.text = item.source_name + _date_label.text = _format_date(item.published) + + _open_button.icon = get_theme_icon("ExternalLink", "EditorIcons") + _open_button.pressed.connect(func() -> void: OS.shell_open(item.link)) + + if item.thumbnail_url.is_empty(): + _thumbnail.texture = DEFAULT_ICON + else: + _load_thumbnail() + + +func set_images_src(images_src: RemoteImageSrc.I) -> void: + _images_src = images_src + if _item and not _item.thumbnail_url.is_empty(): + _load_thumbnail() + + +func _load_thumbnail() -> void: + if _images_src == null or _item.thumbnail_url.is_empty(): + return + _images_src.async_load_img(_item.thumbnail_url, func(tex: Texture2D) -> void: + if tex is ImageTexture: + (tex as ImageTexture).set_size_override(Vector2i(64, 64) * Config.EDSCALE) + _thumbnail.texture = tex + ) + + +func apply_filter(filter: Callable) -> bool: + return filter.call({ + 'name': _item.title, + 'path': _item.source_name, + 'tags': _tags + }) + + +func get_sort_data() -> Dictionary: + return {'ref': self, 'published': _item.published} + + +func _format_date(date_str: String) -> String: + if date_str.is_empty(): + return "" + # ISO 8601: 2026-03-10T14:30:00+00:00 + if "T" in date_str: + var parts := date_str.split("T") + var date_parts := parts[0].split("-") + if date_parts.size() >= 3: + return "%s-%s-%s" % [date_parts[0], date_parts[1], date_parts[2]] + # RFC 2822: Mon, 10 Mar 2026 14:30:00 +0000 + if "," in date_str: + var comma_parts := date_str.split(",") + if comma_parts.size() > 1: + return comma_parts[1].strip_edges().left(16) + return date_str diff --git a/src/components/rss_feed/list/rss_feed_item.gd.uid b/src/components/rss_feed/list/rss_feed_item.gd.uid new file mode 100644 index 00000000..1aa86fe9 --- /dev/null +++ b/src/components/rss_feed/list/rss_feed_item.gd.uid @@ -0,0 +1 @@ +uid://kcj0tywlo0pk diff --git a/src/components/rss_feed/list/rss_feed_item.tscn b/src/components/rss_feed/list/rss_feed_item.tscn new file mode 100644 index 00000000..6c94760a --- /dev/null +++ b/src/components/rss_feed/list/rss_feed_item.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=6 format=3] + +[ext_resource type="Script" path="res://src/components/rss_feed/list/rss_feed_item.gd" id="1_script"] +[ext_resource type="Script" path="res://src/components/misc/list_item_title_label.gd" id="2_title"] +[ext_resource type="Script" path="res://src/components/misc/themed_button.gd" id="3_button"] +[ext_resource type="Script" path="res://src/components/misc/list_item_icon.gd" id="4_icon"] + +[node name="RssFeedItem" type="HBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_script") + +[node name="Thumbnail" type="TextureRect" parent="."] +unique_name_in_owner = true +layout_mode = 2 +script = ExtResource("4_icon") +_stretch_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="TitleLabel" type="Label" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Video Title" +script = ExtResource("2_title") + +[node name="Info" type="HBoxContainer" parent="VBoxContainer"] +modulate = Color(1, 1, 1, 0.6) +layout_mode = 2 + +[node name="SourceLabel" type="Label" parent="VBoxContainer/Info"] +unique_name_in_owner = true +layout_mode = 2 +text = "Source" + +[node name="Spacer" type="Control" parent="VBoxContainer/Info"] +custom_minimum_size = Vector2(10, 0) +layout_mode = 2 + +[node name="DateLabel" type="Label" parent="VBoxContainer/Info"] +unique_name_in_owner = true +layout_mode = 2 +text = "2026-01-01" + +[node name="Spacer2" type="Control" parent="VBoxContainer/Info"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="OpenButton" type="Button" parent="VBoxContainer/Info"] +unique_name_in_owner = true +layout_mode = 2 +flat = true diff --git a/src/components/rss_feed/list/rss_feed_list.gd b/src/components/rss_feed/list/rss_feed_list.gd new file mode 100644 index 00000000..4f221025 --- /dev/null +++ b/src/components/rss_feed/list/rss_feed_list.gd @@ -0,0 +1,17 @@ +extends VBoxList + + +func _post_add(_item_data: Object, _raw_item_control: Control) -> void: + if _raw_item_control is RssFeedItemControl: + var rss_control := get_parent() + if rss_control is RssFeedControl: + var item := _raw_item_control as RssFeedItemControl + item.set_images_src((rss_control as RssFeedControl)._images_src) + + +func _item_comparator(a: Dictionary, b: Dictionary) -> bool: + return true + + +func _fill_sort_options(_btn: OptionButton) -> void: + pass diff --git a/src/components/rss_feed/list/rss_feed_list.gd.uid b/src/components/rss_feed/list/rss_feed_list.gd.uid new file mode 100644 index 00000000..c7a0add9 --- /dev/null +++ b/src/components/rss_feed/list/rss_feed_list.gd.uid @@ -0,0 +1 @@ +uid://dfla7da0fs7j0 diff --git a/src/components/rss_feed/list/rss_feed_list.tscn b/src/components/rss_feed/list/rss_feed_list.tscn new file mode 100644 index 00000000..e5a2cc05 --- /dev/null +++ b/src/components/rss_feed/list/rss_feed_list.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="PackedScene" path="res://src/components/v_box_list/v_box_list.tscn" id="1_base"] +[ext_resource type="Script" path="res://src/components/rss_feed/list/rss_feed_list.gd" id="2_script"] +[ext_resource type="PackedScene" path="res://src/components/rss_feed/list/rss_feed_item.tscn" id="3_item"] + +[node name="RssFeedList" instance=ExtResource("1_base")] +script = ExtResource("2_script") +_item_scene = ExtResource("3_item") +_cached_search_key = "rss_feed_list" + +[node name="SearchBox" parent="HBoxContainer" index="1"] +placeholder_text = "Filter Feed" +clear_button_enabled = true + +[node name="HBoxContainer" parent="HBoxContainer" index="2"] +visible = false diff --git a/src/components/rss_feed/rss_feed.gd b/src/components/rss_feed/rss_feed.gd new file mode 100644 index 00000000..fb251c24 --- /dev/null +++ b/src/components/rss_feed/rss_feed.gd @@ -0,0 +1,169 @@ +class_name RssFeedControl +extends HBoxContainer + +const HOUR = 60 * 60 +const CACHE_LIFETIME_SEC = 6 * HOUR + +const FEED_SOURCES: Array[Dictionary] = [ + { + "name": "Red Indie Games", + "url": "https://www.youtube.com/feeds/videos.xml?channel_id=UCN5oRj4nn1qVRzOadVc97MQ", + }, + { + "name": "GDQuest", + "url": "https://www.youtube.com/feeds/videos.xml?channel_id=UCxboW7x0jZqFdvMdCFKTMsQ", + }, + { + "name": "Godot Engine", + "url": "https://www.youtube.com/feeds/videos.xml?channel_id=UCKIDvfZD1ZhY4_hhbotf7wA", + }, + { + "name": "Godot Blog", + "url": "https://godotengine.org/rss.xml", + }, +] + + +@onready var _feed_list := %RssFeedList as VBoxList +@onready var _refresh_button := %RefreshButton as Button + +var _http_request: HTTPRequest +var _data_loaded := false +var _fetching := false +var _images_src: RemoteImageSrc.I + + +func _ready() -> void: + _http_request = HTTPRequest.new() + add_child(_http_request) + _refresh_button.icon = get_theme_icon("Reload", "EditorIcons") + _refresh_button.pressed.connect(_refetch_data) + _images_src = RemoteImageSrc.LoadFileBuffer.new( + RemoteImageSrc.FileByUrlCachedEtag.new(), + get_theme_icon("FileBrokenBigThumb", "EditorIcons") + ) + + +func init() -> void: + if visible: + _refetch_data() + + +func _refetch_data() -> void: + if _fetching: + return + _fetching = true + _refresh_button.disabled = true + + var all_items: Array = [] + for source in FEED_SOURCES: + var feed_items := await _fetch_feed(source) + all_items.append_array(feed_items) + + # Sort newest first (ISO 8601 and RFC 2822 dates sort lexicographically) + all_items.sort_custom(func(a: RssFeed.FeedItem, b: RssFeed.FeedItem) -> bool: + return a.published > b.published + ) + + _feed_list.refresh(all_items) + _data_loaded = true + _fetching = false + _refresh_button.disabled = false + + +func _fetch_feed(source: Dictionary) -> Array: + var headers := PackedStringArray([Config.AGENT_HEADER]) + _http_request.request(source.url as String, headers, HTTPClient.METHOD_GET) + var response: Array = await _http_request.request_completed + var response_obj := HttpClient.Response.new(response) + + if response_obj.code != 200: + return [] + + var body := XML.parse_buffer(response_obj.body) + if not body or not body.root: + return [] + + return _parse_feed(body.root, source) + + +func _parse_feed(root: XMLNode, source: Dictionary) -> Array: + var items: Array = [] + # YouTube (Atom) uses , standard RSS uses + for child: XMLNode in root.children: + if child.name == "entry" or child.name == "item": + var item := _parse_entry(child, source.name as String) + if item and not item.title.is_empty(): + items.append(item) + return items + + +func _parse_entry(entry: XMLNode, source_name: String) -> RssFeed.FeedItem: + var item := RssFeed.FeedItem.new() + item.source_name = source_name + var smart_entry := exml.smart(entry) + + # Title + var title_node := smart_entry.find_smart_child_recursive( + exml.Filters.by_name("title") + ) + if title_node: + item.title = title_node.o.content + + # Link + # YouTube (Atom): + # RSS: url + var link_node := smart_entry.find_smart_child_recursive( + exml.Filters.by_name("link") + ) + if link_node: + if link_node.o.attributes.has("href"): + item.link = link_node.o.attributes["href"] + else: + item.link = link_node.o.content + + # Published date + # YouTube (Atom): , RSS: + var pub_node := smart_entry.find_smart_child_recursive( + exml.Filters.by_name("published") + ) + if not pub_node: + pub_node = smart_entry.find_smart_child_recursive( + exml.Filters.by_name("pubDate") + ) + if pub_node: + item.published = pub_node.o.content + + # Thumbnail (YouTube): + var thumb_node := _find_deep(entry, "media:thumbnail") + if thumb_node: + item.thumbnail_url = thumb_node.attributes.get("url", "") + + # Description + # YouTube: , RSS: + var desc_node := _find_deep(entry, "media:description") + if not desc_node: + var smart_desc := smart_entry.find_smart_child_recursive( + exml.Filters.by_name("description") + ) + if smart_desc: + desc_node = smart_desc.o + if desc_node: + item.description = desc_node.content + + return item + + +func _find_deep(node: XMLNode, name: String) -> XMLNode: + for child: XMLNode in node.children: + if child.name == name: + return child + var found := _find_deep(child, name) + if found: + return found + return null + + +func _on_visibility_changed() -> void: + if visible and not _data_loaded: + _refetch_data() diff --git a/src/components/rss_feed/rss_feed.gd.uid b/src/components/rss_feed/rss_feed.gd.uid new file mode 100644 index 00000000..bb0c61e5 --- /dev/null +++ b/src/components/rss_feed/rss_feed.gd.uid @@ -0,0 +1 @@ +uid://cvtuorgvycuy diff --git a/src/components/rss_feed/rss_feed.tscn b/src/components/rss_feed/rss_feed.tscn new file mode 100644 index 00000000..53a15dd7 --- /dev/null +++ b/src/components/rss_feed/rss_feed.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://src/components/rss_feed/rss_feed.gd" id="1_script"] +[ext_resource type="PackedScene" path="res://src/components/rss_feed/list/rss_feed_list.tscn" id="2_list"] +[ext_resource type="Script" path="res://src/components/misc/themed_button.gd" id="3_button"] + +[node name="RssFeed" type="HBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_script") + +[node name="RssFeedList" parent="." instance=ExtResource("2_list")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="RefreshButton" type="Button" parent="RssFeedList/HBoxContainer" index="2"] +unique_name_in_owner = true +layout_mode = 2 +flat = true +script = ExtResource("3_button") +_theme_icon_name = "Reload" + +[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] diff --git a/src/components/rss_feed/rss_feed_class.gd b/src/components/rss_feed/rss_feed_class.gd new file mode 100644 index 00000000..a5374c24 --- /dev/null +++ b/src/components/rss_feed/rss_feed_class.gd @@ -0,0 +1,10 @@ +class_name RssFeed + + +class FeedItem extends RefCounted: + var title: String = "" + var link: String = "" + var published: String = "" + var source_name: String = "" + var thumbnail_url: String = "" + var description: String = "" diff --git a/src/components/rss_feed/rss_feed_class.gd.uid b/src/components/rss_feed/rss_feed_class.gd.uid new file mode 100644 index 00000000..489ca459 --- /dev/null +++ b/src/components/rss_feed/rss_feed_class.gd.uid @@ -0,0 +1 @@ +uid://dmhj1tbx1ygnd diff --git a/src/components/settings/settings_window.gd.uid b/src/components/settings/settings_window.gd.uid index 6c0e88c0..376ebdcb 100644 --- a/src/components/settings/settings_window.gd.uid +++ b/src/components/settings/settings_window.gd.uid @@ -1 +1,2 @@ -uid://bmctf4vbsqmki +uid://dev80hhurq376 + diff --git a/src/components/tags/manage_tags.gd.uid b/src/components/tags/manage_tags.gd.uid index 25169e56..0f13cd6d 100644 --- a/src/components/tags/manage_tags.gd.uid +++ b/src/components/tags/manage_tags.gd.uid @@ -1 +1,2 @@ -uid://bnjl2aumswtvs +uid://dlaq7k4u0ml6r + diff --git a/src/components/tags/tag/tag.gd.uid b/src/components/tags/tag/tag.gd.uid index d20279d9..26e31f27 100644 --- a/src/components/tags/tag/tag.gd.uid +++ b/src/components/tags/tag/tag.gd.uid @@ -1 +1,2 @@ -uid://d3rhqbbgnigh4 +uid://fbd5idw0stgp + diff --git a/src/components/title_bar/title_bar.gd.uid b/src/components/title_bar/title_bar.gd.uid index f99848a0..1d01ed5f 100644 --- a/src/components/title_bar/title_bar.gd.uid +++ b/src/components/title_bar/title_bar.gd.uid @@ -1 +1,2 @@ -uid://64nd6b6tekm7 +uid://tair7kfgeji5 + diff --git a/src/components/v_box_list/item/h_box_list_item.gd.uid b/src/components/v_box_list/item/h_box_list_item.gd.uid index 798a99e5..17a37a6b 100644 --- a/src/components/v_box_list/item/h_box_list_item.gd.uid +++ b/src/components/v_box_list/item/h_box_list_item.gd.uid @@ -1 +1,2 @@ -uid://bjmpqhildjiwv +uid://dknlefq2vq2km + diff --git a/src/components/v_box_list/v_box_list.gd.uid b/src/components/v_box_list/v_box_list.gd.uid index 87382024..0e0d3e9e 100644 --- a/src/components/v_box_list/v_box_list.gd.uid +++ b/src/components/v_box_list/v_box_list.gd.uid @@ -1 +1,2 @@ -uid://cduew46se0sjf +uid://ds1mwetp2mkb5 + diff --git a/src/config.gd b/src/config.gd index eeb7c895..1e44d1be 100644 --- a/src/config.gd +++ b/src/config.gd @@ -15,6 +15,8 @@ const DEFAULT_VERSIONS_PATH = "user://versions" const DEFAULT_DOWNLOADS_PATH = "user://downloads" const DEFAULT_UPDATES_PATH = "user://updates" const DEFAULT_CACHE_DIR_PATH = "user://cache" +const TEMPLATES_CONFIG_PATH = "user://templates.cfg" +const DEFAULT_TEMPLATES_PATH = "user://templates" const RELEASES_URL = "https://github.com/MakovWait/godots/releases" const RELEASES_LATEST_API_ENDPOINT = "https://api.github.com/repos/MakovWait/godots/releases/latest" const RELEASES_API_ENDPOINT = "https://api.github.com/repos/MakovWait/godots/releases" diff --git a/src/config.gd.uid b/src/config.gd.uid index 4deacb44..d5d985ee 100644 --- a/src/config.gd.uid +++ b/src/config.gd.uid @@ -1 +1,2 @@ -uid://5yjb8v485qit +uid://68phjoulylio + diff --git a/src/extensions/buttons.gd.uid b/src/extensions/buttons.gd.uid index b16a9c34..bb6d439c 100644 --- a/src/extensions/buttons.gd.uid +++ b/src/extensions/buttons.gd.uid @@ -1 +1,2 @@ -uid://c5sfll5kmuxpj +uid://cam1wuv7iyru6 + diff --git a/src/extensions/dict.gd.uid b/src/extensions/dict.gd.uid index 1e3064f4..fda4a4c3 100644 --- a/src/extensions/dict.gd.uid +++ b/src/extensions/dict.gd.uid @@ -1 +1,2 @@ -uid://ch56ten6rp66d +uid://bfeir7d2kh8a1 + diff --git a/src/extensions/dir.gd.uid b/src/extensions/dir.gd.uid index f58f577f..9606fc82 100644 --- a/src/extensions/dir.gd.uid +++ b/src/extensions/dir.gd.uid @@ -1 +1,2 @@ -uid://xhxgu8irtgm6 +uid://q41v2c53aodc + diff --git a/src/main/gui/gui_main.gd b/src/main/gui/gui_main.gd index 55f2b729..db934525 100644 --- a/src/main/gui/gui_main.gd +++ b/src/main/gui/gui_main.gd @@ -7,6 +7,8 @@ const theme_source = preload("res://theme/theme.gd") @export var _projects: ProjectsControl @export var _asset_lib_projects: AssetLibProjects @export var _godots_releases: GodotsReleasesControl +@export var _rss_feed: RssFeedControl +@export var _project_templates: ProjectTemplatesControl @export var _auto_updates: AutoUpdates @export var _asset_download: PackedScene @export var _title_tabs: BoxContainer @@ -24,6 +26,7 @@ var _on_exit_tree_callbacks: Array[Callable] = [] var _local_remote_switch_context: LocalRemoteEditorsSwitchContext var _local_editors_service: LocalEditors.List var _projects_service: Projects.List +var _templates_service: ProjectTemplates.List func _ready() -> void: @@ -63,8 +66,10 @@ func _ready() -> void: _title_tabs.add_child(TitleTabButton.new("ProjectList", tr("Projects"), _tab_container, [_projects])) _title_tabs.add_child(TitleTabButton.new("AssetLib", tr("Asset Library"), _tab_container, [_asset_lib_projects])) _title_tabs.add_child(TitleTabButton.new("GodotMonochrome", tr("Editors"), _tab_container, [_local_editors, _remote_editors])) + _title_tabs.add_child(TitleTabButton.new("FileList", tr("Templates"), _tab_container, [_project_templates])) #_title_tabs.add_child(TitleTabButton.new("GodotMonochrome", tr("Remote Editors"), _tab_container, _remote_editors)) #_title_tabs.add_child(TitleTabButton.new(null, tr("Updates"), _tab_container, _updates)) + _title_tabs.add_child(TitleTabButton.new("ExternalLink", tr("Feed"), _tab_container, [_rss_feed])) _gui_base.set( "theme_override_styles/panel", @@ -125,10 +130,13 @@ func _ready() -> void: _local_editors_service.load() _projects_service.load() + _templates_service.load() _projects.init(_projects_service) _local_editors.init(_local_editors_service) _remote_editors.init(%DownloadsContainer as DownloadsContainer) + _rss_feed.init() + _project_templates.init(_templates_service, _projects_service) _projects.manage_tags_requested.connect(_popup_manage_tags) _local_editors.manage_tags_requested.connect(_popup_manage_tags) @@ -195,17 +203,24 @@ func _enter_tree() -> void: _local_editors_service, preload("res://assets/default_project_icon.svg") ) + _templates_service = ProjectTemplates.List.new( + Config.TEMPLATES_CONFIG_PATH, + Config.DEFAULT_TEMPLATES_PATH + ) Context.add(self, _local_remote_switch_context) Context.add(self, _local_editors_service) Context.add(self, _projects_service) + Context.add(self, _templates_service) _on_exit_tree_callbacks.append(func() -> void: _local_editors_service.cleanup() _projects_service.cleanup() + _templates_service.cleanup() Context.erase(self, _local_editors_service) Context.erase(self, _projects_service) + Context.erase(self, _templates_service) Context.erase(self, _local_remote_switch_context) ) @@ -345,4 +360,4 @@ class TitleTabButton extends Button: if what == NOTIFICATION_THEME_CHANGED: if _icon_name: self.icon = get_theme_icon(_icon_name, "EditorIcons") - #theme_type_variation = "MainScreenButton" + #theme_type_variation = "MainScreenButton" diff --git a/src/main/gui/gui_main.tscn b/src/main/gui/gui_main.tscn index 6a8bae6f..4953cd80 100644 --- a/src/main/gui/gui_main.tscn +++ b/src/main/gui/gui_main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3 uid="uid://omgg45dpxftx"] +[gd_scene load_steps=18 format=3 uid="uid://omgg45dpxftx"] [ext_resource type="Script" uid="uid://rj237qsrvde4" path="res://src/main/gui/gui_main.gd" id="1_yjveq"] [ext_resource type="PackedScene" uid="uid://cb6u1mub27xo" path="res://src/components/asset_download/asset_download.tscn" id="2_gfvot"] @@ -15,8 +15,10 @@ [ext_resource type="PackedScene" uid="uid://buwhvihmtjeff" path="res://src/components/tags/manage_tags.tscn" id="12_6d5cn"] [ext_resource type="PackedScene" uid="uid://btxxnjmhy4bqw" path="res://src/components/command_viewer/command_viewer.tscn" id="13_mk5sf"] [ext_resource type="PackedScene" uid="uid://b3aprmu6od0wa" path="res://src/components/settings/settings_window.tscn" id="14_3j6hr"] +[ext_resource type="PackedScene" path="res://src/components/rss_feed/rss_feed.tscn" id="15_rss"] +[ext_resource type="PackedScene" path="res://src/components/project_templates/project_templates.tscn" id="16_templates"] -[node name="Main" type="Control" node_paths=PackedStringArray("_remote_editors", "_local_editors", "_projects", "_asset_lib_projects", "_godots_releases", "_auto_updates", "_title_tabs", "_updates", "_tab_container")] +[node name="Main" type="Control" node_paths=PackedStringArray("_remote_editors", "_local_editors", "_projects", "_asset_lib_projects", "_godots_releases", "_rss_feed", "_project_templates", "_auto_updates", "_title_tabs", "_updates", "_tab_container")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -29,6 +31,8 @@ _local_editors = NodePath("GuiBase/MainVBox/Content/VBoxContainer/TabContainer/L _projects = NodePath("GuiBase/MainVBox/Content/VBoxContainer/TabContainer/Local Projects") _asset_lib_projects = NodePath("GuiBase/MainVBox/Content/VBoxContainer/TabContainer/Asset Library Projects") _godots_releases = NodePath("GuiBase/MainVBox/Content/VBoxContainer/TabContainer/Updates") +_rss_feed = NodePath("GuiBase/MainVBox/Content/VBoxContainer/TabContainer/RSS Feed") +_project_templates = NodePath("GuiBase/MainVBox/Content/VBoxContainer/TabContainer/Project Templates") _auto_updates = NodePath("AutoUpdates") _asset_download = ExtResource("2_gfvot") _title_tabs = NodePath("GuiBase/MainVBox/TitleBar/ButtonsContainer") @@ -101,6 +105,14 @@ layout_mode = 2 visible = false layout_mode = 2 +[node name="RSS Feed" parent="GuiBase/MainVBox/Content/VBoxContainer/TabContainer" instance=ExtResource("15_rss")] +visible = false +layout_mode = 2 + +[node name="Project Templates" parent="GuiBase/MainVBox/Content/VBoxContainer/TabContainer" instance=ExtResource("16_templates")] +visible = false +layout_mode = 2 + [node name="DownloadsContainer" parent="GuiBase/MainVBox/Content/VBoxContainer" instance=ExtResource("9_drkdu")] unique_name_in_owner = true layout_mode = 2 diff --git a/src/main/main.gd b/src/main/main.gd index 54607fc0..8fd088a9 100644 --- a/src/main/main.gd +++ b/src/main/main.gd @@ -19,6 +19,12 @@ func _is_cli_mode(args: PackedStringArray) -> bool: if args.is_empty(): return false + var cli_keywords := ["--ghelp", "-gh", "--recent", "-r", "editor", "exec"] + for arg in args: + if cli_keywords.has(arg): + return true + return false + var grammar := CliGrammar.new(GodotsCommands.commands) var first := args[0] diff --git a/src/objects/node_component/comp.gd.uid b/src/objects/node_component/comp.gd.uid index 56b12b99..09b2f266 100644 --- a/src/objects/node_component/comp.gd.uid +++ b/src/objects/node_component/comp.gd.uid @@ -1 +1 @@ -uid://0gh0ybgaykwd +uid://cursddf6qcxgu diff --git a/src/objects/node_component/comp_init.gd.uid b/src/objects/node_component/comp_init.gd.uid index a310dcb2..f43915f2 100644 --- a/src/objects/node_component/comp_init.gd.uid +++ b/src/objects/node_component/comp_init.gd.uid @@ -1 +1 @@ -uid://bthggkihl6m6u +uid://csc6fcl6bu1w8 diff --git a/src/objects/node_component/comp_refs.gd.uid b/src/objects/node_component/comp_refs.gd.uid index 0782700b..089e4007 100644 --- a/src/objects/node_component/comp_refs.gd.uid +++ b/src/objects/node_component/comp_refs.gd.uid @@ -1 +1 @@ -uid://c7i01fbchwogc +uid://blv34t4rda08d diff --git a/src/objects/node_component/comp_scene.gd.uid b/src/objects/node_component/comp_scene.gd.uid index 7eafb719..dcdf3bbf 100644 --- a/src/objects/node_component/comp_scene.gd.uid +++ b/src/objects/node_component/comp_scene.gd.uid @@ -1 +1 @@ -uid://3dngwlwfy3hv +uid://btifplcguvm8j diff --git a/src/objects/os_process_schema.gd.uid b/src/objects/os_process_schema.gd.uid index 55b35173..1582103a 100644 --- a/src/objects/os_process_schema.gd.uid +++ b/src/objects/os_process_schema.gd.uid @@ -1 +1 @@ -uid://3pv24ee0sanu +uid://x161twdo4i7r diff --git a/src/objects/random_project_names.gd.uid b/src/objects/random_project_names.gd.uid index 87bea830..6050715a 100644 --- a/src/objects/random_project_names.gd.uid +++ b/src/objects/random_project_names.gd.uid @@ -1 +1 @@ -uid://bnc0l3jej28c1 +uid://di1rbdpadd2cp diff --git a/src/objects/set.gd.uid b/src/objects/set.gd.uid index ae69e5fe..83a4f9fb 100644 --- a/src/objects/set.gd.uid +++ b/src/objects/set.gd.uid @@ -1 +1 @@ -uid://g6l8feupl7r3 +uid://dveubmjukijpm diff --git a/src/output.gd.uid b/src/output.gd.uid index 2a83fa9e..295144b1 100644 --- a/src/output.gd.uid +++ b/src/output.gd.uid @@ -1 +1 @@ -uid://122t1qkndf55 +uid://cwce0pkshfhmk diff --git a/src/services/godots_downloads.gd.uid b/src/services/godots_downloads.gd.uid index 7b9cc431..2390ea42 100644 --- a/src/services/godots_downloads.gd.uid +++ b/src/services/godots_downloads.gd.uid @@ -1 +1 @@ -uid://bf53lg0ne5y02 +uid://d24j4lt0eed8u diff --git a/src/services/godots_install.gd.uid b/src/services/godots_install.gd.uid index 729dc334..a18efe6d 100644 --- a/src/services/godots_install.gd.uid +++ b/src/services/godots_install.gd.uid @@ -1 +1 @@ -uid://rvjscu1hf0y5 +uid://bhvjk8g5gbeyt diff --git a/src/services/godots_recent_releases.gd.uid b/src/services/godots_recent_releases.gd.uid index 6da6aace..ef35d3de 100644 --- a/src/services/godots_recent_releases.gd.uid +++ b/src/services/godots_recent_releases.gd.uid @@ -1 +1 @@ -uid://b10t78gwbd0ha +uid://cprvs537u6w13 diff --git a/src/services/godots_releases.gd.uid b/src/services/godots_releases.gd.uid index 5866ac07..3149a89f 100644 --- a/src/services/godots_releases.gd.uid +++ b/src/services/godots_releases.gd.uid @@ -1 +1 @@ -uid://dc240ufm5n7hm +uid://ca1vq1is4vtcl diff --git a/src/services/local_editors.gd.uid b/src/services/local_editors.gd.uid index cc5ab9d0..20750f52 100644 --- a/src/services/local_editors.gd.uid +++ b/src/services/local_editors.gd.uid @@ -1 +1 @@ -uid://r7gjtn2se8sp +uid://bu3dhjaebkg2j diff --git a/src/services/project_templates.gd b/src/services/project_templates.gd new file mode 100644 index 00000000..c27c386c --- /dev/null +++ b/src/services/project_templates.gd @@ -0,0 +1,133 @@ +class_name ProjectTemplates + +const _dict = preload("res://src/extensions/dict.gd") + +class List extends RefCounted: + var _cfg := ConfigFile.new() + var _templates: Dictionary[String, Item] = {} + var _cfg_path: String + var _templates_dir: String + + func _init(cfg_path: String, templates_dir: String) -> void: + _cfg_path = cfg_path + _templates_dir = templates_dir + + func add(name: String, description: String, source_project_path: String, tags: Array = []) -> Item: + var id := _generate_id() + var section := "template_%s" % id + var template_zip_path := _templates_dir.path_join("%s.zip" % id) + + var item := Item.new( + section, + ConfigFileSection.new(section, IConfigFileLike.of_config(_cfg)), + template_zip_path + ) + item.name = name + item.description = description + item.source_project_path = source_project_path + item.tags = tags + item.created_at = int(Time.get_unix_time_from_system()) + _templates[id] = item + return item + + func all() -> Array[Item]: + var result: Array[Item] = [] + for x: Item in _templates.values(): + result.append(x) + return result + + func retrieve(id: String) -> Item: + return _templates.get(id, null) + + func has(id: String) -> bool: + return _templates.has(id) + + func erase(id: String) -> void: + var item := _templates.get(id, null) + if item: + # Delete the zip file + if FileAccess.file_exists(item.zip_path): + DirAccess.remove_absolute(item.zip_path) + _templates.erase(id) + _cfg.erase_section("template_%s" % id) + + # TODO type + func get_all_tags() -> Array: + var set := Set.new() + for template: Item in _templates.values(): + for tag: String in template.tags: + set.append(tag.to_lower()) + return set.values() + + func load() -> Error: + cleanup() + DirAccess.make_dir_recursive_absolute(_templates_dir) + var err := _cfg.load(_cfg_path) + if err and err != ERR_FILE_NOT_FOUND: + return err + for section in _cfg.get_sections(): + if section.begins_with("template_"): + var id := section.substr("template_".length()) + _templates[id] = Item.new( + section, + ConfigFileSection.new(section, IConfigFileLike.of_config(_cfg)), + _templates_dir.path_join("%s.zip" % id) + ) + return Error.OK + + func cleanup() -> void: + _dict.clear_and_free(_templates) + + func save() -> Error: + return _cfg.save(_cfg_path) + + func _generate_id() -> String: + return str(int(Time.get_unix_time_from_system() * 1000)) + "_" + str(randi() % 10000) + + +class Item: + signal internals_changed + signal loaded + + var id: String: + get: return _section.name.substr("template_".length()) + + var name: String: + get: return _section.get_value("name", "Unnamed Template") + set(value): _section.set_value("name", value) + + var description: String: + get: return _section.get_value("description", "") + set(value): _section.set_value("description", value) + + var source_project_path: String: + get: return _section.get_value("source_project_path", "") + set(value): _section.set_value("source_project_path", value) + + var zip_path: String: + get: return _zip_path + + var tags: Array: + get: return _section.get_value("tags", []) + set(value): _section.set_value("tags", value) + + var created_at: int: + get: return _section.get_value("created_at", 0) + set(value): _section.set_value("created_at", value) + + var is_zip_valid: bool: + get: return FileAccess.file_exists(_zip_path) + + var _section: ConfigFileSection + var _zip_path: String + + func _init(section_name: String, section: ConfigFileSection, zip_path: String) -> void: + _section = section + _zip_path = zip_path + + func get_formatted_date() -> String: + var datetime := Time.get_datetime_dict_from_unix_time(created_at) + return "%04d-%02d-%02d %02d:%02d" % [ + datetime.year, datetime.month, datetime.day, + datetime.hour, datetime.minute + ] diff --git a/src/services/projects.gd.uid b/src/services/projects.gd.uid index 9922aacc..415bd8d2 100644 --- a/src/services/projects.gd.uid +++ b/src/services/projects.gd.uid @@ -1 +1 @@ -uid://bjknpd0ahfv4h +uid://c0wklqjt7udsw diff --git a/src/services/remote_image_src.gd.uid b/src/services/remote_image_src.gd.uid index 25958e49..8539d940 100644 --- a/src/services/remote_image_src.gd.uid +++ b/src/services/remote_image_src.gd.uid @@ -1 +1 @@ -uid://dqrnvrxtipb62 +uid://dywmvpuacrbrq