diff --git a/src/Preferences/PreferencesDialog.tscn b/src/Preferences/PreferencesDialog.tscn
index d03e2d934..c788f89a2 100644
--- a/src/Preferences/PreferencesDialog.tscn
+++ b/src/Preferences/PreferencesDialog.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=9 format=3 uid="uid://b3hkjj3s6pe4x"]
+[gd_scene load_steps=10 format=3 uid="uid://b3hkjj3s6pe4x"]
[ext_resource type="Script" path="res://src/Preferences/PreferencesDialog.gd" id="1"]
[ext_resource type="Script" path="res://src/Preferences/HandleExtensions.gd" id="2"]
@@ -7,11 +7,13 @@
[ext_resource type="Script" path="res://src/Preferences/HandleThemes.gd" id="5"]
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="7"]
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="8"]
+[ext_resource type="PackedScene" uid="uid://chy5d42l72crk" path="res://src/UI/ExtensionExplorer/Store.tscn" id="8_jmnx8"]
[sub_resource type="ButtonGroup" id="ButtonGroup_8vsfb"]
[node name="PreferencesDialog" type="AcceptDialog"]
title = "Preferences"
+position = Vector2i(0, 36)
size = Vector2i(800, 500)
exclusive = false
popup_window = true
@@ -28,7 +30,7 @@ offset_bottom = -49.0
size_flags_horizontal = 3
theme_override_constants/separation = 20
theme_override_constants/autohide = 0
-split_offset = 150
+split_offset = 125
[node name="List" type="ItemList" parent="HSplitContainer"]
custom_minimum_size = Vector2(85, 0)
@@ -1076,6 +1078,7 @@ tooltip_text = "Specifies the tablet driver being used on Windows. If you have W
mouse_default_cursor_shape = 2
[node name="Extensions" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
+unique_name_in_owner = true
visible = false
layout_mode = 2
script = ExtResource("2")
@@ -1093,6 +1096,10 @@ text = "Extensions"
layout_mode = 2
size_flags_horizontal = 3
+[node name="Explore" type="Button" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/ExtensionsHeader"]
+layout_mode = 2
+text = "Explore Online"
+
[node name="InstalledExtensions" type="ItemList" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions"]
layout_mode = 2
auto_height = true
@@ -1293,7 +1300,18 @@ layout_mode = 2
layout_mode = 2
text = "Pixelorama must be restarted for changes to take effect."
-[node name="Popups" type="Node" parent="."]
+[node name="Popups" type="Control" parent="."]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
[node name="AddExtensionFileDialog" type="FileDialog" parent="Popups"]
mode = 1
@@ -1307,6 +1325,9 @@ access = 2
filters = PackedStringArray("*.pck ; Godot Resource Pack File", "*.zip ;")
show_hidden_files = true
+[node name="Store" parent="Popups" instance=ExtResource("8_jmnx8")]
+transient = true
+
[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
unique_name_in_owner = true
position = Vector2i(0, 36)
@@ -1329,6 +1350,7 @@ vertical_alignment = 1
[connection signal="item_selected" from="HSplitContainer/List" to="." method="_on_List_item_selected"]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Language/System Language" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Language" method="_on_Language_pressed" binds= [1]]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions/ShrinkContainer/ShrinkApplyButton" to="." method="_on_ShrinkApplyButton_pressed"]
+[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/ExtensionsHeader/Explore" to="Popups/Store" method="_on_explore_pressed"]
[connection signal="empty_clicked" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/InstalledExtensions" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_InstalledExtensions_empty_clicked"]
[connection signal="item_selected" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/InstalledExtensions" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_InstalledExtensions_item_selected"]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/HBoxContainer/AddExtensionButton" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_AddExtensionButton_pressed"]
diff --git a/src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd b/src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd
new file mode 100644
index 000000000..33102ccc2
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd
@@ -0,0 +1,185 @@
+class_name ExtensionEntry
+extends Panel
+
+var extension_container: VBoxContainer
+var thumbnail := ""
+var download_link := ""
+var download_path := ""
+var tags := PackedStringArray()
+var is_update := false ## An update instead of download
+
+# node references used in this script
+@onready var ext_name := %ExtensionName as Label
+@onready var ext_discription := %ExtensionDescription as TextEdit
+@onready var small_picture := %Picture as TextureButton
+@onready var enlarged_picture := %Enlarged as TextureRect
+@onready var request_delay := %RequestDelay as Timer
+@onready var thumbnail_request := %ImageRequest as HTTPRequest
+@onready var extension_downloader := %DownloadRequest as HTTPRequest
+@onready var down_button := %DownloadButton as Button
+@onready var progress_bar := %ProgressBar as ProgressBar
+@onready var done_label := %Done as Label
+@onready var alert_dialog := %Alert as AcceptDialog
+
+
+func set_info(info: Dictionary, extension_path: String) -> void:
+ if "name" in info.keys() and "version" in info.keys():
+ ext_name.text = str(info["name"], "-v", info["version"])
+ # check for updates
+ change_button_if_updatable(info["name"], info["version"])
+ # Setting path extension will be "temporarily" downloaded to before install
+ DirAccess.make_dir_recursive_absolute(str(extension_path, "Download/"))
+ download_path = str(extension_path, "Download/", info["name"], ".pck")
+ if "description" in info.keys():
+ ext_discription.text = info["description"]
+ ext_discription.tooltip_text = ext_discription.text
+ if "thumbnail" in info.keys():
+ thumbnail = info["thumbnail"]
+ if "download_link" in info.keys():
+ download_link = info["download_link"]
+ if "tags" in info.keys():
+ tags.append_array(info["tags"])
+
+ # Adding a tiny delay to prevent sending bulk requests
+ request_delay.wait_time = randf() * 2
+ request_delay.start()
+
+
+func _on_RequestDelay_timeout() -> void:
+ request_delay.queue_free() # node no longer needed
+ thumbnail_request.request(thumbnail) # image
+
+
+func _on_ImageRequest_request_completed(
+ _result, _response_code, _headers, body: PackedByteArray
+) -> void:
+ # Update the received image
+ thumbnail_request.queue_free()
+ var image := Image.new()
+ # for images on internet there is a hagh chance that extension is wrong
+ # so check all of them even if they give error
+ var err := image.load_png_from_buffer(body)
+ if err != OK:
+ var err_a := image.load_jpg_from_buffer(body)
+ if err_a != OK:
+ var err_b := image.load_webp_from_buffer(body)
+ if err_b != OK:
+ var err_c := image.load_tga_from_buffer(body)
+ if err_c != OK:
+ image.load_bmp_from_buffer(body)
+ var texture := ImageTexture.create_from_image(image)
+ small_picture.texture_normal = texture
+ small_picture.pressed.connect(enlarge_thumbnail.bind(texture))
+
+
+func _on_Download_pressed() -> void:
+ down_button.disabled = true
+ extension_downloader.download_file = download_path
+ extension_downloader.request(download_link)
+ prepare_progress()
+
+
+## Called after the extension downloader has finished its job
+func _on_DownloadRequest_request_completed(
+ result: int, _response_code: int, _headers: PackedStringArray, _body: PackedByteArray
+) -> void:
+ if result == HTTPRequest.RESULT_SUCCESS:
+ # Add extension
+ extension_container.install_extension(download_path)
+ if is_update:
+ is_update = false
+ announce_done(true)
+ else:
+ alert_dialog.get_node("Text").text = (
+ str(
+ "Unable to Download extension...\nHttp Code: ",
+ result,
+ " (",
+ error_string(result),
+ ")"
+ )
+ . c_unescape()
+ )
+ alert_dialog.popup_centered()
+ announce_done(false)
+ DirAccess.remove_absolute(download_path)
+
+
+## Updates the entry node's UI
+func announce_done(success: bool) -> void:
+ close_progress()
+ down_button.disabled = false
+ if success:
+ done_label.visible = true
+ down_button.text = "Re-Download"
+ done_label.get_node("DoneDelay").start()
+
+
+## Returns true if entry contains ALL tags in tag_array
+func tags_match(tag_array: PackedStringArray) -> bool:
+ if tags.size() > 0:
+ for tag in tag_array:
+ if !tag in tags:
+ return false
+ return true
+ else:
+ if tag_array.size() > 0:
+ return false
+ return true
+
+
+## Updates the entry node's UI if it has an update available
+func change_button_if_updatable(extension_name: String, new_version: float) -> void:
+ for extension in extension_container.extensions.keys():
+ if extension_container.extensions[extension].file_name == extension_name:
+ var old_version = str_to_var(extension_container.extensions[extension].version)
+ if typeof(old_version) == TYPE_FLOAT:
+ if new_version > old_version:
+ down_button.text = "Update"
+ is_update = true
+ elif new_version == old_version:
+ down_button.text = "Re-Download"
+
+
+## Show an enlarged version of the thumbnail
+func enlarge_thumbnail(texture: ImageTexture) -> void:
+ enlarged_picture.texture = texture
+ enlarged_picture.get_parent().popup_centered()
+
+
+## A beautification function that hides the "Done" label bar after some time
+func _on_DoneDelay_timeout() -> void:
+ done_label.visible = false
+
+
+## Progress bar method
+func prepare_progress() -> void:
+ progress_bar.visible = true
+ progress_bar.value = 0
+ progress_bar.get_node("ProgressTimer").start()
+
+
+## Progress bar method
+func update_progress() -> void:
+ var down := extension_downloader.get_downloaded_bytes()
+ var total := extension_downloader.get_body_size()
+ progress_bar.value = (float(down) / float(total)) * 100.0
+
+
+## Progress bar method
+func close_progress() -> void:
+ progress_bar.visible = false
+ progress_bar.get_node("ProgressTimer").stop()
+
+
+## Progress bar method
+func _on_ProgressTimer_timeout() -> void:
+ update_progress()
+
+
+func _manage_enlarded_thumbnail_close() -> void:
+ enlarged_picture.get_parent().hide()
+
+
+func _manage_alert_close() -> void:
+ alert_dialog.hide()
diff --git a/src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn b/src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn
new file mode 100644
index 000000000..23f0a387d
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn
@@ -0,0 +1,125 @@
+[gd_scene load_steps=3 format=3 uid="uid://dnjpemuehkxsn"]
+
+[ext_resource type="Script" path="res://src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd" id="1_3no3v"]
+[ext_resource type="Texture2D" uid="uid://b47r0c6auaqk6" path="res://assets/graphics/icons/icon.png" id="2_qhsve"]
+
+[node name="ExtensionEntry" type="Panel"]
+self_modulate = Color(0.411765, 0.411765, 0.411765, 1)
+custom_minimum_size = Vector2(300, 150)
+offset_right = 284.0
+offset_bottom = 150.0
+size_flags_horizontal = 3
+script = ExtResource("1_3no3v")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="Picture" type="TextureButton" parent="MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(120, 120)
+layout_mode = 2
+mouse_default_cursor_shape = 2
+texture_normal = ExtResource("2_qhsve")
+ignore_texture_size = true
+stretch_mode = 5
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="ExtensionName" type="Label" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Extension Name..."
+
+[node name="ExtensionDescription" type="TextEdit" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+placeholder_text = "Description"
+editable = false
+wrap_mode = 1
+
+[node name="Done" type="Label" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+visible = false
+self_modulate = Color(0.337255, 1, 0, 1)
+layout_mode = 2
+text = "Done!!!"
+
+[node name="DoneDelay" type="Timer" parent="MarginContainer/HBoxContainer/VBoxContainer/Done"]
+wait_time = 2.0
+
+[node name="ProgressBar" type="ProgressBar" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+
+[node name="ProgressTimer" type="Timer" parent="MarginContainer/HBoxContainer/VBoxContainer/ProgressBar"]
+wait_time = 0.1
+
+[node name="DownloadButton" type="Button" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Download"
+
+[node name="RequestDelay" type="Timer" parent="."]
+unique_name_in_owner = true
+one_shot = true
+autostart = true
+
+[node name="ImageRequest" type="HTTPRequest" parent="."]
+unique_name_in_owner = true
+
+[node name="DownloadRequest" type="HTTPRequest" parent="."]
+unique_name_in_owner = true
+
+[node name="Alert" type="AcceptDialog" parent="."]
+unique_name_in_owner = true
+size = Vector2i(421, 106)
+
+[node name="Text" type="Label" parent="Alert"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
+horizontal_alignment = 1
+
+[node name="EnlardedThumbnail" type="Window" parent="."]
+position = Vector2i(0, 36)
+size = Vector2i(440, 360)
+visible = false
+transient = true
+exclusive = true
+
+[node name="Enlarged" type="TextureRect" parent="EnlardedThumbnail"]
+unique_name_in_owner = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+stretch_mode = 5
+
+[connection signal="timeout" from="MarginContainer/HBoxContainer/VBoxContainer/Done/DoneDelay" to="." method="_on_DoneDelay_timeout"]
+[connection signal="timeout" from="MarginContainer/HBoxContainer/VBoxContainer/ProgressBar/ProgressTimer" to="." method="_on_ProgressTimer_timeout"]
+[connection signal="pressed" from="MarginContainer/HBoxContainer/VBoxContainer/DownloadButton" to="." method="_on_Download_pressed"]
+[connection signal="timeout" from="RequestDelay" to="." method="_on_RequestDelay_timeout"]
+[connection signal="request_completed" from="ImageRequest" to="." method="_on_ImageRequest_request_completed"]
+[connection signal="request_completed" from="DownloadRequest" to="." method="_on_DownloadRequest_request_completed"]
+[connection signal="close_requested" from="Alert" to="." method="_manage_alert_close"]
+[connection signal="focus_exited" from="Alert" to="." method="_manage_alert_close"]
+[connection signal="close_requested" from="EnlardedThumbnail" to="." method="_manage_enlarded_thumbnail_close"]
+[connection signal="focus_exited" from="EnlardedThumbnail" to="." method="_manage_enlarded_thumbnail_close"]
diff --git a/src/UI/ExtensionExplorer/Store.gd b/src/UI/ExtensionExplorer/Store.gd
new file mode 100644
index 000000000..b0a6343ac
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Store.gd
@@ -0,0 +1,214 @@
+extends Window
+
+## Usage:
+## Change the "STORE_NAME" and "STORE_LINK"
+## Don't touch anything else
+
+const STORE_NAME := "Extension Explorer"
+# gdlint: ignore=max-line-length
+const STORE_LINK := "https://raw.githubusercontent.com/Orama-Interactive/Pixelorama/master/src/UI/ExtensionExplorer/store_info.md"
+## File that will contain information about extensions available for download
+const STORE_INFORMATION_FILE := STORE_NAME + ".md"
+const EXTENSION_ENTRY_TSCN := preload("res://src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn")
+
+# Variables placed here due to their frequent use
+var extension_container: VBoxContainer
+var extension_path: String ## The path where extensions will be stored (obtained from pixelorama)
+var custom_links_remaining: int ## Remaining custom links to be processed
+var redirects: Array[String]
+var faulty_custom_links: Array[String]
+
+# node references used in this script
+@onready var content: VBoxContainer = $"%Content"
+@onready var store_info_downloader: HTTPRequest = %StoreInformationDownloader
+@onready var main_store_link: LineEdit = %MainStoreLink
+@onready var custom_store_links: VBoxContainer = %CustomStoreLinks
+@onready var search_manager: LineEdit = %SearchManager
+@onready var tab_container: TabContainer = %TabContainer
+@onready var progress_bar: ProgressBar = %ProgressBar
+@onready var update_timer: Timer = %UpdateTimer
+@onready var faulty_links_label: Label = %FaultyLinks
+@onready var custom_link_error: AcceptDialog = %ErrorCustom
+@onready var error_get_info: AcceptDialog = %Error
+
+
+func _ready() -> void:
+ # Basic setup
+ extension_container = Global.preferences_dialog.find_child("Extensions")
+ main_store_link.text = STORE_LINK
+ # Get the path that pixelorama uses to store extensions
+ extension_path = ProjectSettings.globalize_path(extension_container.EXTENSIONS_PATH)
+ # tell the downloader where to download the store information
+ store_info_downloader.download_file = extension_path.path_join(STORE_INFORMATION_FILE)
+
+
+func _on_Store_about_to_show() -> void:
+ # Clear old tags
+ search_manager.available_tags = PackedStringArray()
+ for tag in search_manager.tag_list.get_children():
+ tag.queue_free()
+ # Clear old entries
+ for entry in content.get_children():
+ entry.queue_free()
+ faulty_custom_links.clear()
+ custom_links_remaining = custom_store_links.custom_links.size()
+ fetch_info(STORE_LINK)
+
+
+func _on_close_requested() -> void:
+ hide()
+
+
+func fetch_info(link: String) -> void:
+ if extension_path != "": # Did everything went smoothly in _ready() function?
+ # everything is ready, now request the store information
+ # so that available extensions could be displayed
+ var error := store_info_downloader.request(link)
+ if error == OK:
+ prepare_progress()
+ else:
+ printerr("Unable to get info from remote repository.")
+ error_getting_info(error)
+
+
+## When downloading is finished
+func _on_StoreInformation_request_completed(
+ result: int, _response_code: int, _headers: PackedStringArray, _body: PackedByteArray
+) -> void:
+ if result == HTTPRequest.RESULT_SUCCESS:
+ # process the info contained in the file
+ var file := FileAccess.open(
+ extension_path.path_join(STORE_INFORMATION_FILE), FileAccess.READ
+ )
+ while not file.eof_reached():
+ process_line(file.get_line())
+ file.close()
+
+ DirAccess.remove_absolute(extension_path.path_join(STORE_INFORMATION_FILE))
+ # Hide the progress bar because it's no longer required
+ close_progress()
+ else:
+ printerr("Unable to get info from remote repository...")
+ error_getting_info(result)
+
+
+func close_progress() -> void:
+ progress_bar.get_parent().visible = false
+ tab_container.visible = true
+ update_timer.stop()
+ if redirects.size() > 0:
+ var next_link := redirects.pop_front() as String
+ fetch_info(next_link)
+ else:
+ # no more redirects, jump to the next store
+ custom_links_remaining -= 1
+ if custom_links_remaining >= 0:
+ var next_link: String = custom_store_links.custom_links[custom_links_remaining]
+ fetch_info(next_link)
+ else:
+ if faulty_custom_links.size() > 0: # manage custom faulty links
+ faulty_links_label.text = ""
+ for link in faulty_custom_links:
+ faulty_links_label.text += str(link, "\n")
+ custom_link_error.popup_centered()
+
+
+## Signal connected from StoreButton.tscn
+func _on_explore_pressed() -> void:
+ popup_centered()
+
+
+## Function related to error dialog
+func _on_CopyCommand_pressed() -> void:
+ DisplayServer.clipboard_set(
+ "sudo flatpak override com.orama_interactive.Pixelorama --share=network"
+ )
+
+
+## Adds a new extension entry to the "content"
+func add_entry(info: Dictionary) -> void:
+ var entry := EXTENSION_ENTRY_TSCN.instantiate()
+ entry.extension_container = extension_container
+ content.add_child(entry)
+ entry.set_info(info, extension_path)
+
+
+## Gets called when data couldn't be fetched from remote repository
+func error_getting_info(result: int) -> void:
+ # Shows a popup if error is from main link (i-e MainStore)
+ # Popups for errors in custom_links are handled in close_progress()
+ if custom_links_remaining == custom_store_links.custom_links.size():
+ error_get_info.popup_centered()
+ error_get_info.title = error_string(result)
+ else:
+ faulty_custom_links.append(custom_store_links.custom_links[custom_links_remaining])
+ close_progress()
+
+
+## Progress bar method
+func prepare_progress() -> void:
+ progress_bar.get_parent().visible = true
+ tab_container.visible = false
+ progress_bar.value = 0
+ update_timer.start()
+
+
+## Progress bar method
+func update_progress() -> void:
+ var down := store_info_downloader.get_downloaded_bytes()
+ var total := store_info_downloader.get_body_size()
+ progress_bar.value = (float(down) / float(total)) * 100.0
+
+
+## Progress bar method
+func _on_UpdateTimer_timeout() -> void:
+ update_progress()
+
+
+# DATA PROCESSORS
+func process_line(line: String):
+ # If the line isn't a comment, we will check data type
+ var raw_data
+ line = line.strip_edges()
+ if !line.begins_with("#") and !line.begins_with("//") and line != "":
+ # attempting to convert to a variable other than a string
+ raw_data = str_to_var(line)
+ if !raw_data: # attempt failed, using it as string
+ raw_data = line
+
+ # Determine action based on data type
+ match typeof(raw_data):
+ TYPE_ARRAY:
+ var extension_data: Dictionary = parse_extension_data(raw_data)
+ add_entry(extension_data)
+ TYPE_STRING:
+ # it's most probably a store link
+ var link: String = raw_data.strip_edges()
+ if !link in redirects:
+ redirects.append(link)
+
+
+func parse_extension_data(raw_data: Array) -> Dictionary:
+ DirAccess.make_dir_recursive_absolute(str(extension_path, "Download/"))
+ var result := {}
+ # Check for non-compulsory things if they exist
+ for item in raw_data:
+ if typeof(item) == TYPE_ARRAY:
+ # first array element should always be an identifier text type
+ var identifier = item.pop_front()
+ if typeof(identifier) == TYPE_STRING and item.size() > 0:
+ match identifier:
+ "name":
+ result["name"] = item[0]
+ "version":
+ result["version"] = item[0]
+ "description":
+ result["description"] = item[0]
+ "thumbnail":
+ result["thumbnail"] = item[0]
+ "download_link":
+ result["download_link"] = item[0]
+ "tags": # (this should remain as an array)
+ result["tags"] = item
+ search_manager.add_new_tags(item)
+ return result
diff --git a/src/UI/ExtensionExplorer/Store.tscn b/src/UI/ExtensionExplorer/Store.tscn
new file mode 100644
index 000000000..283a90b3a
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Store.tscn
@@ -0,0 +1,230 @@
+[gd_scene load_steps=5 format=3 uid="uid://chy5d42l72crk"]
+
+[ext_resource type="Script" path="res://src/UI/ExtensionExplorer/Store.gd" id="1_pwcwi"]
+[ext_resource type="Script" path="res://src/UI/ExtensionExplorer/Subscripts/SearchManager.gd" id="2_uqsvm"]
+[ext_resource type="Script" path="res://src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd" id="3_dk1xf"]
+[ext_resource type="Texture2D" uid="uid://d1urikaf1lxwl" path="res://assets/graphics/timeline/new_frame.png" id="4_ntl7p"]
+
+[node name="Store" type="Window"]
+title = "Explore Online"
+position = Vector2i(0, 36)
+size = Vector2i(760, 470)
+visible = false
+wrap_controls = true
+exclusive = true
+script = ExtResource("1_pwcwi")
+
+[node name="TabContainer" type="TabContainer" parent="."]
+unique_name_in_owner = true
+clip_contents = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 7.0
+offset_top = 8.0
+offset_right = -7.0
+offset_bottom = -8.0
+
+[node name="Store" type="MarginContainer" parent="TabContainer"]
+layout_mode = 2
+
+[node name="StoreMainContainer" type="HSplitContainer" parent="TabContainer/Store"]
+layout_mode = 2
+
+[node name="Parameters" type="VBoxContainer" parent="TabContainer/Store/StoreMainContainer"]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+
+[node name="SearchManager" type="LineEdit" parent="TabContainer/Store/StoreMainContainer/Parameters"]
+unique_name_in_owner = true
+layout_mode = 2
+placeholder_text = "Search..."
+script = ExtResource("2_uqsvm")
+
+[node name="Header" type="Label" parent="TabContainer/Store/StoreMainContainer/Parameters"]
+layout_mode = 2
+theme_type_variation = &"HeaderSmall"
+text = "Tags:"
+
+[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Store/StoreMainContainer/Parameters"]
+layout_mode = 2
+size_flags_vertical = 3
+horizontal_scroll_mode = 0
+
+[node name="TagList" type="VBoxContainer" parent="TabContainer/Store/StoreMainContainer/Parameters/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+alignment = 1
+
+[node name="ContentScrollContainer" type="ScrollContainer" parent="TabContainer/Store/StoreMainContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Content" type="VBoxContainer" parent="TabContainer/Store/StoreMainContainer/ContentScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Options" type="MarginContainer" parent="TabContainer"]
+visible = false
+layout_mode = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Options"]
+layout_mode = 2
+
+[node name="CustomStoreLinks" type="VBoxContainer" parent="TabContainer/Options/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+script = ExtResource("3_dk1xf")
+
+[node name="Header" type="Label" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks"]
+layout_mode = 2
+theme_type_variation = &"HeaderSmall"
+text = "Store Links:"
+
+[node name="MainStoreLink" type="LineEdit" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks"]
+unique_name_in_owner = true
+layout_mode = 2
+editable = false
+
+[node name="Links" type="VBoxContainer" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks"]
+layout_mode = 2
+
+[node name="ButtonContainer" type="HBoxContainer" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks"]
+layout_mode = 2
+
+[node name="Guide" type="Button" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer"]
+visible = false
+layout_mode = 2
+text = "Guide to making a Store File"
+
+[node name="NewLink" type="Button" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer"]
+custom_minimum_size = Vector2(24, 24)
+layout_mode = 2
+size_flags_horizontal = 8
+
+[node name="TextureRect" type="TextureRect" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer/NewLink"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 5.0
+offset_top = 5.0
+offset_right = -5.0
+offset_bottom = -5.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = ExtResource("4_ntl7p")
+stretch_mode = 5
+
+[node name="ProgressContainer" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+visible = false
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 51.0
+offset_top = 21.0
+offset_right = -37.0
+offset_bottom = -5.0
+alignment = 1
+
+[node name="ProgressBar" type="ProgressBar" parent="ProgressContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+
+[node name="Label" type="Label" parent="ProgressContainer"]
+layout_mode = 2
+text = "Fetching data from Remote Repository
+Please Wait"
+horizontal_alignment = 1
+
+[node name="UpdateTimer" type="Timer" parent="ProgressContainer"]
+unique_name_in_owner = true
+wait_time = 0.1
+
+[node name="StoreInformationDownloader" type="HTTPRequest" parent="."]
+unique_name_in_owner = true
+
+[node name="Error" type="AcceptDialog" parent="."]
+unique_name_in_owner = true
+position = Vector2i(0, 36)
+size = Vector2i(511, 326)
+unresizable = true
+popup_window = true
+
+[node name="Content" type="VBoxContainer" parent="Error"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Label" type="Label" parent="Error/Content"]
+custom_minimum_size = Vector2(495, 180)
+layout_mode = 2
+text = "Unable to get info from remote repository.
+
+Possible Solutions:
+- Make sure you are connected to the internet.
+- If you are using the Flatpak version of Pixelorama, you need to grant it permission to connect to the internet. To do that, you can run the following command on your terminal:"
+autowrap_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Error/Content"]
+layout_mode = 2
+
+[node name="LineEdit" type="LineEdit" parent="Error/Content/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "sudo flatpak override com.orama_interactive.Pixelorama --share=network"
+editable = false
+
+[node name="CopyCommand" type="Button" parent="Error/Content/HBoxContainer"]
+layout_mode = 2
+text = "Copy"
+
+[node name="Label2" type="Label" parent="Error/Content"]
+custom_minimum_size = Vector2(495, 50)
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Alternatively, you download Flatseal and set permissions for Flatpak apps there."
+autowrap_mode = 3
+
+[node name="ErrorCustom" type="AcceptDialog" parent="."]
+unique_name_in_owner = true
+position = Vector2i(0, 36)
+size = Vector2i(357, 110)
+popup_window = true
+
+[node name="Content" type="VBoxContainer" parent="ErrorCustom"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
+
+[node name="Label" type="Label" parent="ErrorCustom/Content"]
+layout_mode = 2
+text = "Unable to get info from remote repository."
+
+[node name="FaultyLinks" type="Label" parent="ErrorCustom/Content"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[connection signal="about_to_popup" from="." to="." method="_on_Store_about_to_show"]
+[connection signal="close_requested" from="." to="." method="_on_close_requested"]
+[connection signal="text_changed" from="TabContainer/Store/StoreMainContainer/Parameters/SearchManager" to="TabContainer/Store/StoreMainContainer/Parameters/SearchManager" method="_on_SearchManager_text_changed"]
+[connection signal="visibility_changed" from="TabContainer/Options" to="TabContainer/Options/ScrollContainer/CustomStoreLinks" method="_on_Options_visibility_changed"]
+[connection signal="pressed" from="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer/Guide" to="TabContainer/Options/ScrollContainer/CustomStoreLinks" method="_on_Guide_pressed"]
+[connection signal="pressed" from="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer/NewLink" to="TabContainer/Options/ScrollContainer/CustomStoreLinks" method="_on_NewLink_pressed"]
+[connection signal="timeout" from="ProgressContainer/UpdateTimer" to="." method="_on_UpdateTimer_timeout"]
+[connection signal="request_completed" from="StoreInformationDownloader" to="." method="_on_StoreInformation_request_completed"]
+[connection signal="pressed" from="Error/Content/HBoxContainer/CopyCommand" to="." method="_on_CopyCommand_pressed"]
diff --git a/src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd b/src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd
new file mode 100644
index 000000000..55f7c5749
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd
@@ -0,0 +1,47 @@
+extends VBoxContainer
+
+var custom_links := []
+
+
+func _ready() -> void:
+ custom_links = Global.config_cache.get_value("ExtensionExplorer", "custom_links", [])
+ for link in custom_links:
+ add_field(link)
+
+
+func update_links() -> void:
+ custom_links.clear()
+ for child in $Links.get_children():
+ if child.text != "":
+ custom_links.append(child.text)
+ Global.config_cache.set_value("ExtensionExplorer", "custom_links", custom_links)
+
+
+func _on_NewLink_pressed() -> void:
+ add_field()
+
+
+func add_field(link := "") -> void:
+ var link_field := LineEdit.new()
+ # gdlint: ignore=max-line-length
+ link_field.placeholder_text = "Paste Store link, given by the store owner (will automatically be removed if left empty)"
+ link_field.text = link
+ $Links.add_child(link_field)
+ link_field.text_changed.connect(field_text_changed)
+
+
+func field_text_changed(_text: String) -> void:
+ update_links()
+
+
+func _on_Options_visibility_changed() -> void:
+ for child in $Links.get_children():
+ if child.text == "":
+ child.queue_free()
+
+
+# Uncomment it when we have a proper guide for writing a store_info file
+func _on_Guide_pressed() -> void:
+ pass
+# gdlint: ignore=max-line-length
+# OS.shell_open("https://github.com/Variable-Interactive/Variable-Store/tree/master#rules-for-writing-a-store_info-file")
diff --git a/src/UI/ExtensionExplorer/Subscripts/SearchManager.gd b/src/UI/ExtensionExplorer/Subscripts/SearchManager.gd
new file mode 100644
index 000000000..ca7027829
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Subscripts/SearchManager.gd
@@ -0,0 +1,50 @@
+extends LineEdit
+
+var available_tags := PackedStringArray()
+@onready var tag_list: VBoxContainer = $"%TagList"
+
+
+func _on_SearchManager_text_changed(_new_text: String) -> void:
+ tag_text_search()
+
+
+func tag_text_search() -> void:
+ var result := text_search(text)
+ var tags := PackedStringArray([])
+ for tag: Button in tag_list.get_children():
+ if tag.button_pressed:
+ tags.append(tag.text)
+
+ for entry in result:
+ if !entry.tags_match(tags):
+ entry.visible = false
+
+
+func text_search(text_to_search: String) -> Array[ExtensionEntry]:
+ var result: Array[ExtensionEntry] = []
+ for entry: ExtensionEntry in $"%Content".get_children():
+ var visibility := true
+ if text_to_search != "":
+ var extension_name := entry.ext_name.text.to_lower()
+ var extension_description := entry.ext_discription.text.to_lower()
+ if not text_to_search.to_lower() in extension_name:
+ if not text_to_search.to_lower() in extension_description:
+ visibility = false
+ if visibility == true:
+ result.append(entry)
+ entry.visible = visibility
+ return result
+
+
+func add_new_tags(tag_array: PackedStringArray) -> void:
+ for tag in tag_array:
+ if !tag in available_tags:
+ available_tags.append(tag)
+ var tag_checkbox := CheckBox.new()
+ tag_checkbox.text = tag
+ tag_list.add_child(tag_checkbox)
+ tag_checkbox.toggled.connect(start_tag_search)
+
+
+func start_tag_search(_button_pressed: bool) -> void:
+ tag_text_search()
diff --git a/src/UI/ExtensionExplorer/store_info.md b/src/UI/ExtensionExplorer/store_info.md
new file mode 100644
index 000000000..bb0354595
--- /dev/null
+++ b/src/UI/ExtensionExplorer/store_info.md
@@ -0,0 +1,27 @@
+// This file is for online use.
+
+## Rules for writing a (store_info) file:
+// 1. The Store Entry is one large Array (referred to as "entry") consisting of sub-arrays (referred to as "data")
+// e.g `[[keyword, ....], [keyword, ....], [keyword, ....], .......]`
+// 2. Each data must have a keyword of type `String` at it's first index which helps in identifying what the data represents.
+// e.g, ["name", "name of extension"] is the data giving information about "name".
+// Valid keywords are `name`, `version`, `description`, `tags`, `thumbnail`, `download_link`
+// Put quotation marks ("") to make it a string, otherwise error will occur.
+// 3. One store entry must occupy only one line (and vice-versa).
+// 4. Comments are supported. you can comment an entire line by placing `#` or `//` at the start of the line (comments between or at end of line are not allowed).
+// 5. links to another store_info file can be placed inside another store_info file (it will get detected as a custom store file).
+
+## TIPS:
+// - `thumbnail` is the link you get by right clicking an image (uploaded somewhere on the internet) and selecting Copy Image Link.
+// - `download_link` is ususlly od the form `{repo}/raw/{Path of extension within repo}`
+// e.g, if `https://github.com/Variable-ind/Pixelorama-Extensions/blob/master/Extensions/Example.pck` is the URL path to your extension then replace "blob" with "raw"
+// and the link becomes `"https://github.com/Variable-ind/Pixelorama-Extensions/raw/master/Extensions/Example.pck"`
+
+// For further help see the entries below for reference of how it's done
+## Entries:
+// Put Official Extensions Here
+
+
+## Other Store Links:
+### VariableStore
+https://raw.githubusercontent.com/Variable-ind/Pixelorama-Extensions/4.0/store_info.md
diff --git a/src/UI/Timeline/AnimationTimeline.tscn b/src/UI/Timeline/AnimationTimeline.tscn
index 2271ccc0f..6c0ecd273 100644
--- a/src/UI/Timeline/AnimationTimeline.tscn
+++ b/src/UI/Timeline/AnimationTimeline.tscn
@@ -953,7 +953,7 @@ mouse_filter = 2
color = Color(0, 0.741176, 1, 0.501961)
[node name="PasteTagPopup" type="Popup" parent="."]
-size = Vector2i(250, 574)
+size = Vector2i(250, 335)
min_size = Vector2i(250, 0)
script = ExtResource("12")
@@ -992,6 +992,7 @@ layout_mode = 2
text = "Create new tags"
[node name="Instructions" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
+custom_minimum_size = Vector2(250, 23)
layout_mode = 2
text = "Available tags:"
autowrap_mode = 3
@@ -1004,6 +1005,7 @@ size_flags_vertical = 3
[node name="StartFrame" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
unique_name_in_owner = true
+custom_minimum_size = Vector2(250, 23)
layout_mode = 2
horizontal_alignment = 1
autowrap_mode = 3