Skip to content

Commit

Permalink
feat: import functionality of plugin and installer code
Browse files Browse the repository at this point in the history
  • Loading branch information
myyk committed Apr 22, 2024
1 parent ea97a74 commit 9925395
Show file tree
Hide file tree
Showing 12 changed files with 1,198 additions and 17 deletions.
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
# Project specific ignores
icon.jpg.import
addons/plugin_updater/generated

# Godot 4+ specific ignores
.godot/

# Godot specific ignores
.import/
export.cfg
export_presets.cfg

# gdUnit4 specific ignores
reports/
addons/gdUnit4/tmp-update/

# Imported translations (automatically generated from CSV files)
*.translation

# Mono-specific ignores
.mono/
data_*/
mono_crash.*.json
12 changes: 0 additions & 12 deletions addons/plugin-updater/plugin-updater.gd

This file was deleted.

193 changes: 193 additions & 0 deletions addons/plugin_updater/core/download_update_panel.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
@tool
extends Window

## Updater heaviliy inspired by GDUnit4's updater, but decoupled completely from GDUnit. Also did not
## include all the patching that included since it seemed to complicated to include for most projects.

#TODO: read this from somewhere
var config = UpdaterConfig.get_user_config()

var spinner_icon = "res://addons/%s/updater2/spinner.tres" % config.plugin_name

# Using this style of import avoids polluting user's namespaces
const UpdaterConfig = preload("updater_config.gd")
const HttpClient = preload("updater_http_client.gd")
const MarkDownReader = preload("updater_markdown_reader.gd")

const TEMP_FILE_NAME = "user://temp.zip"

@onready var _md_reader: MarkDownReader = MarkDownReader.new()
@onready var _http_client: HttpClient = $HttpClient
@onready var _header: Label = $Panel/GridContainer/PanelContainer/header
@onready var _content: RichTextLabel = $Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content
@onready var _update_button: Button = $Panel/GridContainer/Panel/HBoxContainer/update

var _download_zip_url: String

func _ready():
hide()
_http_client.github_repo = config.github_repo

var plugin :EditorPlugin = Engine.get_meta(config.editor_plugin_meta)

# wait a bit to allow the editor to initialize itself
await Engine.get_main_loop().create_timer(float(config.secs_before_check_for_update)).timeout

_check_for_updater()

func _check_for_updater():
var response = await _http_client.request_latest_version()
if response.code() != 200:
push_warning("Update information cannot be retrieved from GitHub! \n %s" % response.response())
return
var latest_version := extract_latest_version(response)
var current_version := extract_current_version()

# if same version exit here no update need
if latest_version.is_greater(current_version):
_download_zip_url = extract_zip_url(response)
_header.text = "Current version '%s'. A new version '%s' is available" % [current_version, latest_version]
await show_update()

func show_update() -> void:
message_h4("\n\n\nRequest release infos ... [img=24x24]%s[/img]" % spinner_icon, Color.SNOW)
popup_centered_ratio(.5)
prints("Scanning for %s Update ..." % config.plugin_name)
var content :String

var response: HttpClient.HttpResponse = await _http_client.request_releases()
if response.code() == 200:
content = await extract_releases(response, extract_current_version())
else:
message_h4("\n\n\nError checked request available releases!", Color.RED)
return

# finally force rescan to import images as textures
if Engine.is_editor_hint():
await rescan()
message(content, Color.DODGER_BLUE)
_update_button.set_disabled(false)

func rescan() -> void:
if Engine.is_editor_hint():
if OS.is_stdout_verbose():
prints(".. reimport release resources")
var fs := EditorInterface.get_resource_filesystem()
fs.scan()
while fs.is_scanning():
if OS.is_stdout_verbose():
progress_bar(fs.get_scanning_progress() * 100 as int)
await Engine.get_main_loop().process_frame
await Engine.get_main_loop().process_frame
await Engine.get_main_loop().create_timer(1).timeout

func extract_current_version() -> UpdaterSemVer:
var config_file = ConfigFile.new()
config_file.load('addons/%s/plugin.cfg' % config.plugin_name)
return UpdaterSemVer.parse(config_file.get_value('plugin', 'version'))

static func extract_latest_version(response: HttpClient.HttpResponse) -> UpdaterSemVer:
var body :Array = response.response()
return UpdaterSemVer.parse(body[0]["name"])

static func extract_zip_url(response: HttpClient.HttpResponse) -> String:
var body :Array = response.response()
return body[0]["zipball_url"]

func extract_releases(response: HttpClient.HttpResponse, current_version) -> String:
await get_tree().process_frame
var result := ""
for release in response.response():
if UpdaterSemVer.parse(release["tag_name"]).equals(current_version):
break
var release_description :String = release["body"]
result += await _md_reader.to_bbcode(release_description)
return result

func message_h4(message :String, color :Color, clear := true) -> void:
if clear:
_content.clear()
_content.append_text("[font_size=16]%s[/font_size]" % _colored(message, color))

func message(message :String, color :Color) -> void:
_content.clear()
_content.append_text(_colored(message, color))

func progress_bar(p_progress :int, p_color :Color = Color.POWDER_BLUE):
if p_progress < 0:
p_progress = 0
if p_progress > 100:
p_progress = 100
printraw("scan [%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "#").rpad(50, "-"), p_progress])

func _colored(message :String, color :Color) -> String:
return "[color=#%s]%s[/color]" % [color.to_html(), message]

func _on_disable_updates_toggled(toggled_on):
# TODO: Store a setting somewhere
pass

func _on_update_pressed():
hide()
_update_button.set_disabled(true)

#TODO: How do I give the plugins a hook to perform actions before updating?

#TODO: Perform the update, maybe use the simpler approach from the dialog plugin
var updater_http_request = HTTPRequest.new()
updater_http_request.accept_gzip = true
add_child(updater_http_request)

updater_http_request.request_completed.connect(_on_http_request_request_completed)
updater_http_request.request(_download_zip_url)

func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS:
message_h4("\n\n\nError downloading update!", Color.RED)
return

# Save the downloaded zip
var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
zip_file.store_buffer(body)
zip_file.close()

OS.move_to_trash(ProjectSettings.globalize_path("res://addons/%s" % config.plugin_name))

var zip_reader: ZIPReader = ZIPReader.new()
zip_reader.open(TEMP_FILE_NAME)
var files: PackedStringArray = zip_reader.get_files()

var base_path = files[1]
# Remove archive folder
files.remove_at(0)
# Remove assets folder
files.remove_at(0)

for path in files:
var new_file_path: String = path.replace(base_path, "")
if path.ends_with("/"):
DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path)
else:
var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE)
file.store_buffer(zip_reader.read_file(path))

zip_reader.close()
DirAccess.remove_absolute(TEMP_FILE_NAME)

#TODO: Show that we successfully updated

func _on_close_pressed():
hide()

func _on_content_meta_clicked(meta :String):
var properties = str_to_var(meta)
if properties.has("url"):
OS.shell_open(properties.get("url"))

func _on_content_meta_hover_started(meta :String):
var properties = str_to_var(meta)
if properties.has("tool_tip"):
_content.set_tooltip_text(properties.get("tool_tip"))

func _on_content_meta_hover_ended(meta):
_content.set_tooltip_text("")
422 changes: 422 additions & 0 deletions addons/plugin_updater/core/download_update_panel.tscn

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions addons/plugin_updater/core/spinner.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[gd_resource type="AnimatedTexture" load_steps=9 format=3 uid="uid://dljx0fmjlrbuh"]

[ext_resource type="Texture2D" uid="uid://ddxpytkht0m5p" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress1.svg" id="1_ctgew"]
[ext_resource type="Texture2D" uid="uid://dowca7ike2thl" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress2.svg" id="2_xwye8"]
[ext_resource type="Texture2D" uid="uid://cwh8md6qipmdw" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress3.svg" id="3_53vx7"]
[ext_resource type="Texture2D" uid="uid://dm0jpqdjetv2c" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress4.svg" id="4_eecnj"]
[ext_resource type="Texture2D" uid="uid://bkj6kjyjyi7cd" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress5.svg" id="5_auay0"]
[ext_resource type="Texture2D" uid="uid://bsljbs1aiyels" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress6.svg" id="6_fx28b"]
[ext_resource type="Texture2D" uid="uid://cct6crbhix7u8" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress7.svg" id="7_giugp"]
[ext_resource type="Texture2D" uid="uid://dqc521iq12a7l" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress8.svg" id="8_yppa0"]

[resource]
frames = 8
speed_scale = 2.5
frame_0/texture = ExtResource("1_ctgew")
frame_0/duration = 0.2
frame_1/texture = ExtResource("2_xwye8")
frame_1/duration = 0.2
frame_2/texture = ExtResource("3_53vx7")
frame_2/duration = 0.2
frame_3/texture = ExtResource("4_eecnj")
frame_3/duration = 0.2
frame_4/texture = ExtResource("5_auay0")
frame_4/duration = 0.2
frame_5/texture = ExtResource("6_fx28b")
frame_5/duration = 0.2
frame_6/texture = ExtResource("7_giugp")
frame_6/duration = 0.2
frame_7/texture = ExtResource("8_yppa0")
frame_7/duration = 0.2
17 changes: 17 additions & 0 deletions addons/plugin_updater/core/updater_config.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
extends RefCounted

## TODO: Placeholder config

static func get_user_config() -> Dictionary:
var user_config: Dictionary = {
plugin_name = "plugin_updater",
secs_before_check_for_update = 5,
github_repo = "myyk/godot-playlists",
editor_plugin_meta = "PlaylistsEditorPlugin", #TODO: try to eliminate this one
}

#if FileAccess.file_exists(DialogueConstants.USER_CONFIG_PATH):
#var file: FileAccess = FileAccess.open(DialogueConstants.USER_CONFIG_PATH, FileAccess.READ)
#user_config.merge(JSON.parse_string(file.get_as_text()), true)

return user_config
79 changes: 79 additions & 0 deletions addons/plugin_updater/core/updater_http_client.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
@tool
extends Node

## Credits: Mostly copied from https://github.com/MikeSchulze/gdUnit4/blob/99b7c323f443e5fcc67f9a79b4df532727e8986f/addons/gdUnit4/src/update/GdUnitUpdateClient.gd

signal request_completed(response)

@export var github_repo = ""

class HttpResponse:
var _code :int
var _body :PackedByteArray


func _init(code_ :int, body_ :PackedByteArray):
_code = code_
_body = body_

func code() -> int:
return _code

func response() -> Variant:
var test_json_conv := JSON.new()
test_json_conv.parse(_body.get_string_from_utf8())
return test_json_conv.get_data()

func body() -> PackedByteArray:
return _body

var _http_request :HTTPRequest = HTTPRequest.new()

func _ready():
add_child(_http_request)
_http_request.connect("request_completed", Callable(self, "_on_request_completed"))


func _notification(what):
if what == NOTIFICATION_PREDELETE:
if is_instance_valid(_http_request):
_http_request.queue_free()


func request_latest_version() -> HttpResponse:
var error = _http_request.request("https://api.github.com/repos/%s/tags" % github_repo)
if error != OK:
var message = "request_latest_version failed: %d" % error
return HttpResponse.new(error, message.to_utf8_buffer())
return await self.request_completed


func request_releases() -> HttpResponse:
var error = _http_request.request("https://api.github.com/repos/%s/releases" % github_repo)
if error != OK:
var message = "request_releases failed: %d" % error
return HttpResponse.new(error, message.to_utf8_buffer())
return await self.request_completed


func request_image(url :String) -> HttpResponse:
var error = _http_request.request(url)
if error != OK:
var message = "request_image failed: %d" % error
return HttpResponse.new(error, message.to_utf8_buffer())
return await self.request_completed


func request_zip_package(url :String, file :String) -> HttpResponse:
_http_request.set_download_file(file)
var error = _http_request.request(url)
if error != OK:
var message = "request_zip_package failed: %d" % error
return HttpResponse.new(error, message.to_utf8_buffer())
return await self.request_completed


func _on_request_completed(_result :int, response_code :int, _headers :PackedStringArray, body :PackedByteArray):
if _http_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
_http_request.set_download_file("")
request_completed.emit(HttpResponse.new(response_code, body))
Loading

0 comments on commit 9925395

Please sign in to comment.