Compare commits
14 Commits
f7776aab2d
...
main
Author | SHA1 | Date | |
---|---|---|---|
f942c91cf6
|
|||
34dbb91570
|
|||
ee5f270bbe
|
|||
97b1aad886
|
|||
14b56cf5ed
|
|||
ec3996a6c6
|
|||
fee4edce61
|
|||
ba3e2751dd
|
|||
040a9ad1af
|
|||
db5700b62d
|
|||
94c256b608
|
|||
f58dc7f37b
|
|||
9173250580
|
|||
898759fecf
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
.env
|
22
Classes/DotEnv.gd
Normal file
22
Classes/DotEnv.gd
Normal file
@ -0,0 +1,22 @@
|
||||
extends Node
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var path = ""
|
||||
if OS.has_feature("editor"):
|
||||
path = ProjectSettings.globalize_path("res://.env")
|
||||
else:
|
||||
path = OS.get_executable_path().get_base_dir().path_join(".env")
|
||||
|
||||
if !FileAccess.file_exists(path):
|
||||
return
|
||||
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
while !f.eof_reached():
|
||||
var line := f.get_line()
|
||||
var split := line.split("=")
|
||||
OS.set_environment(split[0].strip_edges(), split[1].strip_edges())
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
queue_free()
|
@ -1,5 +1,4 @@
|
||||
extends Resource
|
||||
class_name GroupListMetadata
|
||||
|
||||
@export var order: Array[String]
|
||||
@export var uncollapsed_groups: Array[String]
|
||||
@export var groups: Array[GroupMetadata]
|
||||
|
@ -1,7 +1,11 @@
|
||||
extends Resource
|
||||
class_name GroupMetadata
|
||||
|
||||
@export var name: String
|
||||
@export var icon_path: String
|
||||
@export var description: String
|
||||
@export var installs: Array[InstallMetadata]
|
||||
@export var installs: Array[Dictionary]
|
||||
@export var settings_overrides: Dictionary
|
||||
@export var index: int
|
||||
|
||||
signal save_request
|
||||
|
59
Classes/GroupsInstallsManager.gd
Normal file
59
Classes/GroupsInstallsManager.gd
Normal file
@ -0,0 +1,59 @@
|
||||
extends Node
|
||||
|
||||
const GROUPS_BASE_FOLDER := "user://groups"
|
||||
const METADATA_FILENAME := "meta.tres"
|
||||
|
||||
# key: GroupMetadata
|
||||
# value: String = UUID
|
||||
var groups: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DirAccess.make_dir_absolute(GROUPS_BASE_FOLDER)
|
||||
|
||||
var d := DirAccess.open(GROUPS_BASE_FOLDER)
|
||||
d.list_dir_begin()
|
||||
var current_folder := d.get_next()
|
||||
while current_folder != "":
|
||||
var res_path := GROUPS_BASE_FOLDER.path_join(current_folder).path_join(METADATA_FILENAME)
|
||||
var gm: GroupMetadata = ResourceLoader.load(res_path, "GroupMetadata")
|
||||
groups[gm] = current_folder
|
||||
current_folder = d.get_next()
|
||||
|
||||
|
||||
func create_group_folder(gm: GroupMetadata) -> void:
|
||||
if gm in groups:
|
||||
return
|
||||
|
||||
var folder_name := UUID.v4()
|
||||
var d := DirAccess.open(GROUPS_BASE_FOLDER)
|
||||
d.make_dir(folder_name)
|
||||
|
||||
groups[gm] = folder_name
|
||||
gm.save_request.connect(group_metadata_should_save.bind(gm))
|
||||
|
||||
|
||||
func delete_group_folder(gm: GroupMetadata) -> void:
|
||||
if !groups.has(gm):
|
||||
return
|
||||
|
||||
var path := ProjectSettings.globalize_path(GROUPS_BASE_FOLDER.path_join(groups[gm]))
|
||||
OS.move_to_trash(path)
|
||||
groups.erase(gm)
|
||||
|
||||
|
||||
func group_metadata_should_save(gm: GroupMetadata) -> void:
|
||||
var path := GROUPS_BASE_FOLDER.path_join(groups[gm]).path_join(METADATA_FILENAME)
|
||||
var save_err := ResourceSaver.save(gm, path)
|
||||
if save_err != OK:
|
||||
print("couldn't save resource, error ", save_err)
|
||||
|
||||
|
||||
func get_all_group_metadatas() -> Array[GroupMetadata]:
|
||||
var res: Array[GroupMetadata]
|
||||
res.assign(groups.keys())
|
||||
res.sort_custom(
|
||||
func(a: GroupMetadata, b: GroupMetadata):
|
||||
return a.index < b.index
|
||||
)
|
||||
return res
|
@ -1,7 +1,30 @@
|
||||
extends Resource
|
||||
class_name InstallMetadata
|
||||
|
||||
@export var name: String
|
||||
@export var icon_path: String
|
||||
@export var binary_path: String
|
||||
@export var local_overrides: Dictionary
|
||||
var name: String
|
||||
var icon_path: String
|
||||
var binary_path: String
|
||||
var local_overrides: Dictionary
|
||||
var pids: Array[int] = []
|
||||
var index: int = 0
|
||||
|
||||
|
||||
func to_d() -> Dictionary:
|
||||
return {
|
||||
"name": name,
|
||||
"icon_path": icon_path,
|
||||
"binary_path": binary_path,
|
||||
"local_overrides": local_overrides,
|
||||
"pids": pids,
|
||||
"index": index,
|
||||
}
|
||||
|
||||
|
||||
static func from_d(d: Dictionary) -> InstallMetadata:
|
||||
var res := InstallMetadata.new()
|
||||
res.name = d.get("name", "")
|
||||
res.icon_path = d.get("icon_path", "")
|
||||
res.binary_path = d.get("binary_path", "")
|
||||
res.local_overrides = d.get("local_overrides", {})
|
||||
res.pids = d.get("pids", [])
|
||||
res.index = d.get("index", 0)
|
||||
return res
|
||||
|
447
Classes/ReleasesManager.gd
Normal file
447
Classes/ReleasesManager.gd
Normal file
@ -0,0 +1,447 @@
|
||||
extends Node
|
||||
|
||||
const API_SLUG := "https://api.github.com/repos/"
|
||||
const RELEASES_SLUG := "godotengine/godot/releases"
|
||||
const ASSET_SLUG := "assets/%s"
|
||||
|
||||
const RELEASES_BASE_FOLDER := "user://releases"
|
||||
const RELEASES_FILE := RELEASES_BASE_FOLDER + "/releases.json"
|
||||
const TEMP_FOLDER := "user://_tmp/"
|
||||
|
||||
const FETCH_COOLDOWN := 60.0 # seconds
|
||||
|
||||
var releases: Releases
|
||||
var github_token: String = OS.get_environment("GITHUB_TOKEN")
|
||||
|
||||
var http: HTTPRequest = HTTPRequest.new()
|
||||
var is_downloading: bool = false
|
||||
|
||||
var threads: Array[Thread]
|
||||
|
||||
signal releases_fetched(releases: Releases)
|
||||
signal version_downloaded(version: String)
|
||||
signal export_templates_downloaded(version: String)
|
||||
signal status_update(status: String)
|
||||
|
||||
enum Status{
|
||||
IDLE,
|
||||
RELEASE_DOWNLOAD_IN_PROGRESS,
|
||||
TEMPLATE_DOWNLOAD_IN_PROGRESS,
|
||||
DOWNLOAD_DONE,
|
||||
FETCHING_RELEASES,
|
||||
FETCHING_DONE,
|
||||
VERSION_NOT_IN_INDEX,
|
||||
RELEASE_ALREADY_EXISTS,
|
||||
TEMPLATE_ALREADY_EXISTS,
|
||||
UNPACKING_TEMPLATES,
|
||||
UNPACKING_RELEASE,
|
||||
RELEASE_INSTALLED,
|
||||
TEMPLATE_INSTALLED,
|
||||
}
|
||||
|
||||
const STATUS_MESSAGES := {
|
||||
Status.IDLE: "Idle",
|
||||
Status.RELEASE_DOWNLOAD_IN_PROGRESS: "Downloading Godot v%s",
|
||||
Status.TEMPLATE_DOWNLOAD_IN_PROGRESS: "Downloading Export Templates for v%s",
|
||||
Status.DOWNLOAD_DONE: "Done downloading",
|
||||
Status.FETCHING_RELEASES: "Fetching releases",
|
||||
Status.FETCHING_DONE: "Done fetching releases",
|
||||
Status.VERSION_NOT_IN_INDEX: "Version not in index. Fetch first",
|
||||
Status.RELEASE_ALREADY_EXISTS: "This version already exists",
|
||||
Status.TEMPLATE_ALREADY_EXISTS: "Templates for this version already exist",
|
||||
Status.UNPACKING_TEMPLATES: "Unpacking Export Templates for v%s",
|
||||
Status.UNPACKING_RELEASE: "Unpacking Godot v%s",
|
||||
Status.RELEASE_INSTALLED: "Godot v%s installed",
|
||||
Status.TEMPLATE_INSTALLED: "Export Templates for Godot v%s installed",
|
||||
}
|
||||
|
||||
func _ready() -> void:
|
||||
DirAccess.make_dir_absolute(TEMP_FOLDER)
|
||||
DirAccess.make_dir_absolute(RELEASES_BASE_FOLDER)
|
||||
add_child(http)
|
||||
releases = load_releases()
|
||||
clean_tmp()
|
||||
|
||||
|
||||
func fetch_releases(force_local: bool = false) -> void:
|
||||
if force_local && FileAccess.file_exists(RELEASES_FILE):
|
||||
releases_fetched.emit(releases)
|
||||
return
|
||||
|
||||
if is_downloading:
|
||||
print("Another download is in progress, try again later")
|
||||
return
|
||||
|
||||
if releases.last_checked_at + FETCH_COOLDOWN > Time.get_unix_time_from_system():
|
||||
print("already fetched recently, returning local")
|
||||
releases_fetched.emit(releases)
|
||||
return
|
||||
|
||||
var response_func = func(result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray):
|
||||
if result != OK:
|
||||
return
|
||||
|
||||
releases.clear()
|
||||
releases.last_checked_at = Time.get_unix_time_from_system()
|
||||
var d: Array = JSON.parse_string(body.get_string_from_ascii())
|
||||
for i in d:
|
||||
# tag name is version name
|
||||
var rm := releases.get_or_create_version_metadata(i.tag_name)
|
||||
|
||||
# we already have this one
|
||||
if rm.binary_github_asset_id != -1:
|
||||
continue
|
||||
|
||||
var file_name_placeholder := "Godot_v%s.zip"
|
||||
# only linux for now
|
||||
var file_name_template: String = "%s_linux.x86_64" % i.tag_name
|
||||
var file_name_template_x11: String = "%s_x11.64" % i.tag_name
|
||||
var file_name := file_name_placeholder % file_name_template
|
||||
var file_name_x11 := file_name_placeholder % file_name_template_x11
|
||||
|
||||
var export_template: String = "Godot_v%s_export_templates.tpz" % i.tag_name
|
||||
# go through assets of this release and find our os
|
||||
for asset in i.assets:
|
||||
if asset.name == file_name || asset.name == file_name_x11:
|
||||
rm.binary_github_asset_id = asset.id
|
||||
rm.binary_github_filename = asset.name
|
||||
if asset.name == export_template:
|
||||
rm.export_github_asset_id = asset.id
|
||||
rm.export_github_filename = asset.name
|
||||
|
||||
# if we still have an empty item, it's too old to use
|
||||
# (meaning it's not mirrored on github)
|
||||
# so we remove it
|
||||
if rm.is_empty():
|
||||
releases.releases.erase(i.tag_name)
|
||||
|
||||
releases_fetched.emit(releases)
|
||||
status_update.emit(STATUS_MESSAGES[Status.FETCHING_DONE])
|
||||
is_downloading = false
|
||||
save_releases()
|
||||
|
||||
var headers := [
|
||||
"Accept: application/vnd.github+json",
|
||||
"Authorization: Bearer %s" % github_token,
|
||||
"X-GitHub-Api-Version: 2022-11-28",
|
||||
]
|
||||
is_downloading = true
|
||||
status_update.emit(STATUS_MESSAGES[Status.FETCHING_RELEASES])
|
||||
http.request(API_SLUG + RELEASES_SLUG, PackedStringArray(headers))
|
||||
http.request_completed.connect(response_func, CONNECT_ONE_SHOT)
|
||||
|
||||
|
||||
func download_release(version: String) -> void:
|
||||
if is_downloading:
|
||||
print("downloading something, try again later")
|
||||
return
|
||||
|
||||
if !releases.releases.has(version):
|
||||
# print("this version is not in the index yet or does not exist. fetch first")
|
||||
status_update.emit(STATUS_MESSAGES[Status.VERSION_NOT_IN_INDEX])
|
||||
return
|
||||
|
||||
if (releases.releases[version] as ReleaseMetadata).binary_path != "":
|
||||
# print("already have this version")
|
||||
status_update.emit(STATUS_MESSAGES[Status.RELEASE_ALREADY_EXISTS])
|
||||
return
|
||||
|
||||
var rm: ReleaseMetadata = releases.releases[version]
|
||||
var response_func = func(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray, filename: String):
|
||||
if response_code == 200:
|
||||
# download happens in place.
|
||||
# we know the file is zip, so save it to temp first before unpacking
|
||||
status_update.emit(STATUS_MESSAGES[Status.DOWNLOAD_DONE])
|
||||
print("got it in place")
|
||||
print("downloading url: ", rm.binary_github_filename)
|
||||
var zip_filename := TEMP_FOLDER.path_join("%s.zip" % version)
|
||||
print("making zip: ", zip_filename)
|
||||
var f := FileAccess.open(zip_filename, FileAccess.WRITE)
|
||||
if f.get_error() != OK:
|
||||
print("zip file creation error:", f.get_error())
|
||||
return
|
||||
f.store_buffer(body)
|
||||
f.close()
|
||||
|
||||
var zip := ZIPReader.new()
|
||||
var zerr := zip.open(zip_filename)
|
||||
if zerr != OK:
|
||||
print("error opening zip file: ", zerr)
|
||||
return
|
||||
var res := zip.read_file(filename.trim_suffix(".zip"))
|
||||
zip.close()
|
||||
clean_tmp()
|
||||
status_update.emit(STATUS_MESSAGES[Status.UNPACKING_RELEASE] % version)
|
||||
var d := DirAccess.open(RELEASES_BASE_FOLDER)
|
||||
d.make_dir_recursive(version)
|
||||
var new_file := RELEASES_BASE_FOLDER.path_join(version)
|
||||
new_file = new_file.path_join("godot")
|
||||
f = FileAccess.open(new_file, FileAccess.WRITE)
|
||||
f.store_buffer(res)
|
||||
f.close()
|
||||
rm.binary_path = new_file
|
||||
version_downloaded.emit(version)
|
||||
status_update.emit(STATUS_MESSAGES[Status.RELEASE_INSTALLED] % version)
|
||||
is_downloading = false
|
||||
save_releases()
|
||||
|
||||
elif response_code == 302:
|
||||
# not in place
|
||||
print("tried downloading, but got a 302 which is not currently supported")
|
||||
|
||||
var headers = [
|
||||
"Accept: application/octet-stream",
|
||||
"Authorization: Bearer %s" % github_token,
|
||||
"X-GitHub-Api-Version: 2022-11-28",
|
||||
]
|
||||
var asset_url = API_SLUG.path_join(RELEASES_SLUG).path_join(ASSET_SLUG) % rm.binary_github_asset_id
|
||||
status_update.emit(STATUS_MESSAGES[Status.RELEASE_DOWNLOAD_IN_PROGRESS] % version)
|
||||
is_downloading = true
|
||||
http.request(asset_url, PackedStringArray(headers))
|
||||
http.request_completed.connect(response_func.bind(rm.binary_github_filename), CONNECT_ONE_SHOT)
|
||||
|
||||
|
||||
func download_export_templates(version: String) -> void:
|
||||
if is_downloading:
|
||||
print("downloading something, try again later")
|
||||
return
|
||||
|
||||
if !releases.releases.has(version):
|
||||
# print("this version is not in the index yet or does not exist. fetch first")
|
||||
status_update.emit(STATUS_MESSAGES[Status.VERSION_NOT_IN_INDEX])
|
||||
return
|
||||
|
||||
if (releases.releases[version] as ReleaseMetadata).export_templates_path != "":
|
||||
# print("already have templates for this version")
|
||||
status_update.emit(STATUS_MESSAGES[Status.TEMPLATE_ALREADY_EXISTS])
|
||||
return
|
||||
|
||||
var rm: ReleaseMetadata = releases.releases[version]
|
||||
var response_func = func(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray):
|
||||
if response_code == 200:
|
||||
status_update.emit(STATUS_MESSAGES[Status.DOWNLOAD_DONE])
|
||||
print("got it in place")
|
||||
print("downloading url: ", rm.export_github_filename)
|
||||
var zip_filename := TEMP_FOLDER.path_join("%s_export.tpz" % version)
|
||||
print("making zip: ", zip_filename)
|
||||
var f := FileAccess.open(zip_filename, FileAccess.WRITE)
|
||||
if f.get_error() != OK:
|
||||
print("zip file creation error: ", f.get_error())
|
||||
return
|
||||
f.store_buffer(body)
|
||||
f.close()
|
||||
|
||||
var zip := ZIPReader.new()
|
||||
var zerr := zip.open(zip_filename)
|
||||
if zerr != OK:
|
||||
print("error opening zip file: ", zerr)
|
||||
return
|
||||
var files := zip.get_files()
|
||||
var d := DirAccess.open(RELEASES_BASE_FOLDER)
|
||||
d.make_dir_recursive(version.path_join("templates"))
|
||||
var templates_folder := RELEASES_BASE_FOLDER.path_join(version)
|
||||
status_update.emit(STATUS_MESSAGES[Status.UNPACKING_TEMPLATES] % version)
|
||||
for file in files:
|
||||
var t := Thread.new()
|
||||
threads.append(t)
|
||||
t.start(unpack_zip_file.bind(zip_filename, file, templates_folder.path_join(file), t, version))
|
||||
|
||||
zip.close()
|
||||
# don't clean tmp just yet, since there might be other threads
|
||||
# that are still unpacking from the same zip.
|
||||
# side note: why do we have to have the zip in filesystem instead
|
||||
# of memory? this would be much easier...
|
||||
rm.export_templates_path = ProjectSettings.globalize_path(templates_folder.path_join("templates"))
|
||||
export_templates_downloaded.emit(version)
|
||||
is_downloading = false
|
||||
save_releases()
|
||||
elif response_code == 302:
|
||||
# not in place
|
||||
print("tried downloading, but got a 302 which is not currently supported")
|
||||
|
||||
var headers = [
|
||||
"Accept: application/octet-stream",
|
||||
"Authorization: Bearer %s" % github_token,
|
||||
"X-GitHub-Api-Version: 2022-11-28",
|
||||
]
|
||||
var asset_url = API_SLUG.path_join(RELEASES_SLUG).path_join(ASSET_SLUG) % rm.export_github_asset_id
|
||||
status_update.emit(STATUS_MESSAGES[Status.TEMPLATE_DOWNLOAD_IN_PROGRESS] % version)
|
||||
is_downloading = true
|
||||
http.request(asset_url, PackedStringArray(headers))
|
||||
http.request_completed.connect(response_func, CONNECT_ONE_SHOT)
|
||||
|
||||
|
||||
func unpack_zip_file(zip_file: String, from_file: String, dest_file: String, thread: Thread, version: String) -> void:
|
||||
print("extracting file ", from_file, " to: ", dest_file)
|
||||
var z := ZIPReader.new()
|
||||
var f := z.open(zip_file)
|
||||
if f != OK:
|
||||
print("error opening zip file: ", zip_file, " from thread ", thread, ", exiting early.")
|
||||
_clean_threads.call_deferred(thread)
|
||||
return
|
||||
|
||||
var file := FileAccess.open(dest_file, FileAccess.WRITE)
|
||||
file.store_buffer(z.read_file(from_file))
|
||||
file.close()
|
||||
z.close()
|
||||
|
||||
_clean_threads.call_deferred(thread, version)
|
||||
|
||||
|
||||
func _clean_threads(thread: Thread, version: String) -> void:
|
||||
threads.erase(thread)
|
||||
thread.wait_to_finish()
|
||||
|
||||
if threads.is_empty():
|
||||
status_update.emit(STATUS_MESSAGES[Status.TEMPLATE_INSTALLED] % version)
|
||||
clean_tmp()
|
||||
|
||||
|
||||
func save_releases() -> void:
|
||||
var f := FileAccess.open(RELEASES_FILE, FileAccess.WRITE)
|
||||
f.store_string(releases.to_json())
|
||||
|
||||
|
||||
func load_releases() -> Releases:
|
||||
if !FileAccess.file_exists(RELEASES_FILE):
|
||||
return Releases.new()
|
||||
|
||||
var f := FileAccess.open(RELEASES_FILE, FileAccess.READ)
|
||||
return Releases.from_json(f.get_as_text())
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
for t in threads:
|
||||
t.wait_to_finish()
|
||||
|
||||
clean_tmp()
|
||||
|
||||
|
||||
func get_version_metadata(version: String) -> ReleaseMetadata:
|
||||
return releases.releases.get(version)
|
||||
|
||||
|
||||
func get_installed_versions() -> Dictionary:
|
||||
var res: Dictionary = {}
|
||||
for version in releases.releases:
|
||||
if is_version_installed(version):
|
||||
res[version] = releases.releases[version]
|
||||
return res
|
||||
|
||||
|
||||
func clean_tmp() -> void:
|
||||
var d := DirAccess.open(TEMP_FOLDER)
|
||||
if !d:
|
||||
return
|
||||
|
||||
var to_delete: Array[String] = []
|
||||
d.list_dir_begin()
|
||||
var file_name := d.get_next()
|
||||
while file_name != "":
|
||||
to_delete.append(file_name)
|
||||
file_name = d.get_next()
|
||||
d.list_dir_end()
|
||||
|
||||
for f in to_delete:
|
||||
d.remove(f)
|
||||
|
||||
|
||||
# helper methods
|
||||
func is_version_installed(version: String) -> bool:
|
||||
return get_version_metadata(version).binary_path != ""
|
||||
|
||||
|
||||
func is_version_templates_installed(version) -> bool:
|
||||
return get_version_metadata(version).export_templates_path != ""
|
||||
|
||||
|
||||
class ReleaseMetadata:
|
||||
var binary_path: String
|
||||
var is_mono: bool
|
||||
var export_templates_path: String
|
||||
|
||||
var binary_github_asset_id: int = -1
|
||||
var binary_github_filename: String
|
||||
var export_github_asset_id: int = -1
|
||||
var export_github_filename: String
|
||||
|
||||
|
||||
func is_empty() -> bool:
|
||||
return \
|
||||
binary_github_asset_id == -1 && export_github_asset_id == -1
|
||||
|
||||
func to_d() -> Dictionary:
|
||||
return {
|
||||
"binary_path": binary_path,
|
||||
"is_mono": is_mono,
|
||||
"export_templates_path": export_templates_path,
|
||||
"binary_github_asset_id": binary_github_asset_id,
|
||||
"export_github_asset_id": export_github_asset_id,
|
||||
"binary_github_filename": binary_github_filename,
|
||||
"export_github_filename": export_github_filename,
|
||||
}
|
||||
|
||||
|
||||
static func from_d(d: Dictionary) -> ReleaseMetadata:
|
||||
var res := ReleaseMetadata.new()
|
||||
res.binary_path = d.binary_path
|
||||
res.is_mono = d.is_mono
|
||||
res.export_templates_path = d.export_templates_path
|
||||
res.binary_github_asset_id = d.binary_github_asset_id
|
||||
res.export_github_asset_id = d.export_github_asset_id
|
||||
res.binary_github_filename = d.binary_github_filename
|
||||
res.export_github_filename = d.export_github_filename
|
||||
return res
|
||||
|
||||
|
||||
class Releases:
|
||||
# version[String]: ReleaseMetadata
|
||||
var releases: Dictionary
|
||||
var last_checked_at: float
|
||||
var os: String
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
for r in releases.keys():
|
||||
if (releases[r] as ReleaseMetadata).binary_path == "":
|
||||
releases.erase(r)
|
||||
|
||||
|
||||
func get_or_create_version_metadata(version: String) -> ReleaseMetadata:
|
||||
if releases.has(version):
|
||||
return releases[version]
|
||||
|
||||
var r := ReleaseMetadata.new()
|
||||
releases[version] = r
|
||||
return r
|
||||
|
||||
|
||||
func is_empty() -> bool:
|
||||
return releases.is_empty()
|
||||
|
||||
|
||||
func to_json() -> String:
|
||||
var releases_dict := {}
|
||||
for i in releases:
|
||||
releases_dict[i] = (releases[i] as ReleaseMetadata).to_d()
|
||||
|
||||
var d := {
|
||||
"os": os,
|
||||
"last_checked_at": last_checked_at,
|
||||
"releases": releases_dict,
|
||||
}
|
||||
|
||||
return JSON.stringify(d, "\t", false)
|
||||
|
||||
|
||||
static func from_json(json: String) -> Releases:
|
||||
var d: Dictionary = JSON.parse_string(json)
|
||||
var releases_dict := {}
|
||||
for i in d.releases:
|
||||
releases_dict[i] = ReleaseMetadata.from_d(d.releases[i])
|
||||
|
||||
var r := Releases.new()
|
||||
r.releases = releases_dict
|
||||
r.os = d.os
|
||||
r.last_checked_at = d.last_checked_at
|
||||
return r
|
88
Classes/UUID.gd
Normal file
88
Classes/UUID.gd
Normal file
@ -0,0 +1,88 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Xavier Sellier
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Note: The code might not be as pretty it could be, since it's written
|
||||
# in a way that maximizes performance. Methods are inlined and loops are avoided.
|
||||
class_name UUID
|
||||
const BYTE_MASK: int = 0b11111111
|
||||
|
||||
|
||||
static func uuidbin() -> Array:
|
||||
randomize()
|
||||
# 16 random bytes with the bytes on index 6 and 8 modified
|
||||
return [
|
||||
randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
|
||||
randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK,
|
||||
((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
|
||||
randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
|
||||
]
|
||||
|
||||
static func uuidbinrng(rng: RandomNumberGenerator) -> Array:
|
||||
rng.randomize()
|
||||
return [
|
||||
rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK,
|
||||
rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40, rng.randi() & BYTE_MASK,
|
||||
((rng.randi() & BYTE_MASK) & 0x3f) | 0x80, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK,
|
||||
rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK,
|
||||
]
|
||||
|
||||
static func v4() -> String:
|
||||
# 16 random bytes with the bytes on index 6 and 8 modified
|
||||
var b = uuidbin()
|
||||
|
||||
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [
|
||||
# low
|
||||
b[0], b[1], b[2], b[3],
|
||||
|
||||
# mid
|
||||
b[4], b[5],
|
||||
|
||||
# hi
|
||||
b[6], b[7],
|
||||
|
||||
# clock
|
||||
b[8], b[9],
|
||||
|
||||
# clock
|
||||
b[10], b[11], b[12], b[13], b[14], b[15]
|
||||
]
|
||||
|
||||
static func v4_rng(rng: RandomNumberGenerator) -> String:
|
||||
# 16 random bytes with the bytes on index 6 and 8 modified
|
||||
var b = uuidbinrng(rng)
|
||||
|
||||
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [
|
||||
# low
|
||||
b[0], b[1], b[2], b[3],
|
||||
|
||||
# mid
|
||||
b[4], b[5],
|
||||
|
||||
# hi
|
||||
b[6], b[7],
|
||||
|
||||
# clock
|
||||
b[8], b[9],
|
||||
|
||||
# clock
|
||||
b[10], b[11], b[12], b[13], b[14], b[15]
|
||||
]
|
@ -1,6 +1,8 @@
|
||||
extends VBoxContainer
|
||||
class_name GroupMenu
|
||||
|
||||
const ITEM_SCENE := preload("res://UI/Components/InstallItem.tscn")
|
||||
|
||||
@onready var collapse_button: Button = %CollapseButton
|
||||
@onready var header_icon: TextureRect = %HeaderIcon
|
||||
@onready var header_text_label: Label = %HeaderTextLabel
|
||||
@ -16,13 +18,13 @@ class_name GroupMenu
|
||||
|
||||
@onready var header_container: HBoxContainer = %HeaderContainer
|
||||
|
||||
@onready var install_edit_dialog: InstallEditDialog = %InstallEditDialog
|
||||
|
||||
var group_metadata: GroupMetadata
|
||||
|
||||
signal add_install_button_pressed
|
||||
|
||||
signal edit_group_button_pressed
|
||||
signal delete_button_pressed
|
||||
signal header_text_changed(new_text: String, old_text: String)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@ -32,18 +34,54 @@ func _ready() -> void:
|
||||
set_children_visible(b.button_pressed)
|
||||
)
|
||||
|
||||
add_install_button.pressed.connect(func(): add_install_button_pressed.emit())
|
||||
|
||||
add_install_button.pressed.connect(add_new_install)
|
||||
install_edit_dialog.canceled.connect(install_edit_dialog.clear_ok_button_connections)
|
||||
# edit_group_button.pressed.connect(setup_rename)
|
||||
edit_group_button.pressed.connect(func(): edit_group_button_pressed.emit())
|
||||
delete_group_button.pressed.connect(func(): delete_button_pressed.emit())
|
||||
delete_group_button.pressed.connect(_on_delete_button_pressed)
|
||||
|
||||
header_text_label.gui_input.connect(_on_header_text_label_gui_input)
|
||||
|
||||
|
||||
func add(item: InstallItem) -> int:
|
||||
child_container.add_child(item)
|
||||
return child_container.get_child_count()
|
||||
func add_new_install() -> void:
|
||||
install_edit_dialog.clear()
|
||||
install_edit_dialog.ok_button_text = "Create"
|
||||
|
||||
install_edit_dialog.confirmed.connect(
|
||||
func():
|
||||
var im := install_edit_dialog.get_install_metadata()
|
||||
var install_item: InstallItem = ITEM_SCENE.instantiate()
|
||||
child_container.add_child(install_item)
|
||||
install_item.setup_from_metadata(im)
|
||||
install_item.edit_item_button_pressed.connect(edit_install.bind(install_item))
|
||||
im.index = group_metadata.installs.size()
|
||||
group_metadata.installs.append(im.to_d())
|
||||
group_metadata.save_request.emit()
|
||||
install_item.about_to_delete.connect(_on_install_item_about_to_delete)
|
||||
, CONNECT_ONE_SHOT)
|
||||
|
||||
install_edit_dialog.show()
|
||||
|
||||
|
||||
func add_install_from_metadata(im: InstallMetadata) -> void:
|
||||
var install_item: InstallItem = ITEM_SCENE.instantiate()
|
||||
child_container.add_child(install_item)
|
||||
install_item.setup_from_metadata(im)
|
||||
install_item.about_to_delete.connect(_on_install_item_about_to_delete)
|
||||
install_item.edit_item_button_pressed.connect(edit_install.bind(install_item))
|
||||
|
||||
|
||||
func edit_install(install_item: InstallItem) -> void:
|
||||
install_edit_dialog.clear()
|
||||
install_edit_dialog.ok_button_text = "OK"
|
||||
install_edit_dialog.install_metadata = install_item.install_metadata
|
||||
install_edit_dialog.fill_from_install_metadata()
|
||||
|
||||
install_edit_dialog.confirmed.connect(
|
||||
func():
|
||||
install_item.setup_from_metadata(install_edit_dialog.get_install_metadata())
|
||||
, CONNECT_ONE_SHOT)
|
||||
install_edit_dialog.show()
|
||||
|
||||
|
||||
func free_item(at_idx: int) -> void:
|
||||
@ -87,8 +125,9 @@ func set_children_visible(p_visible: bool) -> void:
|
||||
child_container.visible = p_visible
|
||||
add_install_button_container.visible = p_visible
|
||||
inner_separator.visible = p_visible
|
||||
|
||||
description_label.visible = p_visible && !description_label.text.is_empty()
|
||||
group_metadata.set_meta("collapsed", !p_visible)
|
||||
group_metadata.save_request.emit()
|
||||
|
||||
|
||||
func add_custom(control: Control) -> void:
|
||||
@ -114,9 +153,9 @@ func setup_rename() -> void:
|
||||
|
||||
l.text_submitted.connect(
|
||||
func(new_text: String):
|
||||
var old_text: = header_text_label.text
|
||||
header_text_label.text = new_text
|
||||
header_text_changed.emit(new_text, old_text)
|
||||
group_metadata.name = new_text
|
||||
group_metadata.save_request.emit()
|
||||
|
||||
reenable.call()
|
||||
)
|
||||
@ -124,11 +163,17 @@ func setup_rename() -> void:
|
||||
l.grab_focus()
|
||||
|
||||
|
||||
func setup_from_metadata(header_text: String, p_group_metadata: GroupMetadata) -> void:
|
||||
func setup_from_metadata(p_group_metadata: GroupMetadata) -> void:
|
||||
group_metadata = p_group_metadata
|
||||
set_text(header_text)
|
||||
set_text(p_group_metadata.name)
|
||||
set_description(group_metadata.description)
|
||||
# TODO: add installs
|
||||
set_children_visible(!group_metadata.get_meta("collapsed", false))
|
||||
group_metadata.index = get_index()
|
||||
|
||||
# add installs
|
||||
for i in group_metadata.installs:
|
||||
add_install_from_metadata(InstallMetadata.from_d(i))
|
||||
|
||||
# TODO: add icon
|
||||
|
||||
|
||||
@ -139,3 +184,16 @@ func _on_header_text_label_gui_input(event: InputEvent) -> void:
|
||||
get_viewport().set_input_as_handled()
|
||||
setup_rename()
|
||||
|
||||
|
||||
func _on_delete_button_pressed() -> void:
|
||||
GroupsInstallsManager.delete_group_folder(group_metadata)
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_install_item_about_to_delete(im: InstallMetadata) -> void:
|
||||
group_metadata.installs.remove_at(im.index)
|
||||
for i in group_metadata.installs.size():
|
||||
group_metadata.installs[i]["index"] = i
|
||||
(child_container.get_child(i) as InstallItem).install_metadata.index = i
|
||||
|
||||
group_metadata.save_request.emit()
|
||||
|
@ -1,7 +1,8 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://bh1nf1xrowlpv"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://bh1nf1xrowlpv"]
|
||||
|
||||
[ext_resource type="Script" path="res://UI/Components/GroupMenu.gd" id="1_h3o1w"]
|
||||
[ext_resource type="Texture2D" uid="uid://de57eeeobflp4" path="res://icon.svg" id="2_crg6a"]
|
||||
[ext_resource type="PackedScene" uid="uid://u0de32p0bb61" path="res://UI/Dialogs/InstallEditDialog.tscn" id="3_bapn7"]
|
||||
|
||||
[node name="GroupMenu" type="VBoxContainer"]
|
||||
offset_right = 299.0
|
||||
@ -46,11 +47,13 @@ layout_mode = 2
|
||||
[node name="EditGroupButton" type="Button" parent="HeaderContainer/HeaderCustomControlContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Edit"
|
||||
text = "E"
|
||||
|
||||
[node name="DeleteGroupButton" type="Button" parent="HeaderContainer/HeaderCustomControlContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Delete"
|
||||
text = "X"
|
||||
|
||||
[node name="DescriptionLabel" type="Label" parent="."]
|
||||
@ -78,3 +81,6 @@ text = "New Install"
|
||||
[node name="HSeparator3" type="HSeparator" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="InstallEditDialog" parent="." instance=ExtResource("3_bapn7")]
|
||||
unique_name_in_owner = true
|
||||
|
@ -10,15 +10,15 @@ class_name InstallItem
|
||||
var install_metadata: InstallMetadata
|
||||
|
||||
signal edit_item_button_pressed
|
||||
signal delete_item_button_pressed
|
||||
signal launch_item_button_pressed
|
||||
signal about_to_delete(install_metadata: InstallMetadata)
|
||||
|
||||
signal name_changed(new_name: String)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
edit_item_button.pressed.connect(func(): edit_item_button_pressed.emit())
|
||||
delete_item_button.pressed.connect(func(): delete_item_button_pressed.emit())
|
||||
delete_item_button.pressed.connect(_on_delete_button_pressed)
|
||||
|
||||
name_label.gui_input.connect(_on_name_label_gui_input)
|
||||
|
||||
@ -27,6 +27,10 @@ func set_icon(icon: Texture2D) -> void:
|
||||
item_icon.texture = icon
|
||||
|
||||
|
||||
func set_text(n: String) -> void:
|
||||
name_label.text = n
|
||||
|
||||
|
||||
func set_icon_visible(p_visible: bool) -> void:
|
||||
item_icon.visible = p_visible
|
||||
|
||||
@ -47,6 +51,7 @@ func setup_rename() -> void:
|
||||
func(new_text: String):
|
||||
name_label.text = new_text
|
||||
name_changed.emit(new_text)
|
||||
install_metadata.name = new_text
|
||||
|
||||
name_label.visible = true
|
||||
)
|
||||
@ -60,3 +65,14 @@ func _on_name_label_gui_input(event: InputEvent) -> void:
|
||||
(event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT:
|
||||
get_viewport().set_input_as_handled()
|
||||
setup_rename()
|
||||
|
||||
|
||||
func setup_from_metadata(p_metadata: InstallMetadata) -> void:
|
||||
install_metadata = p_metadata
|
||||
set_text(install_metadata.name)
|
||||
# TODO: add icon
|
||||
|
||||
|
||||
func _on_delete_button_pressed() -> void:
|
||||
about_to_delete.emit(install_metadata)
|
||||
queue_free()
|
||||
|
@ -10,6 +10,7 @@ script = ExtResource("1_sh2ev")
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="Indent" type="MarginContainer" parent="."]
|
||||
custom_minimum_size = Vector2(30, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 6
|
||||
|
||||
@ -26,21 +27,25 @@ layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 1
|
||||
vertical_alignment = 1
|
||||
clip_text = true
|
||||
|
||||
[node name="EditItemButton" type="Button" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Edit"
|
||||
text = "E"
|
||||
|
||||
[node name="DeleteItemButton" type="Button" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Delete"
|
||||
text = "X"
|
||||
|
||||
[node name="LaunchItemButton" type="Button" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = ">"
|
||||
tooltip_text = "Launch"
|
||||
text = "Launch"
|
||||
|
||||
[node name="Indent2" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
@ -14,6 +14,7 @@ text = "Editor Settings overrides"
|
||||
[node name="OverrideItemContainer" type="VBoxContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
47
UI/Components/ReleaseItem.gd
Normal file
47
UI/Components/ReleaseItem.gd
Normal file
@ -0,0 +1,47 @@
|
||||
extends HBoxContainer
|
||||
class_name ReleaseItem
|
||||
|
||||
@onready var version_label: Label = %VersionLabel
|
||||
@onready var install_button: Button = %InstallButton
|
||||
@onready var install_templates_button: Button = %InstallTemplatesButton
|
||||
|
||||
var version: String
|
||||
|
||||
signal install_button_pressed(version)
|
||||
signal install_templates_button_pressed(version)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
install_button.pressed.connect(func(): install_button_pressed.emit(version))
|
||||
install_templates_button.pressed.connect(func(): install_templates_button_pressed.emit(version))
|
||||
|
||||
|
||||
func set_version(v: String) -> void:
|
||||
version = v
|
||||
version_label.text = "Godot v%s" % v
|
||||
|
||||
|
||||
func set_install_button_disabled(d: bool) -> void:
|
||||
install_button.disabled = d
|
||||
|
||||
|
||||
func set_templates_button_disabled(d: bool) -> void:
|
||||
install_templates_button.disabled = d
|
||||
|
||||
|
||||
func _on_version_downloaded(p_version: String) -> void:
|
||||
if p_version != version:
|
||||
# this isn't us
|
||||
return
|
||||
|
||||
set_install_button_disabled(true)
|
||||
ReleasesManager.version_downloaded.disconnect(_on_version_downloaded)
|
||||
|
||||
|
||||
func _on_export_templates_downloaded(p_version: String) -> void:
|
||||
if p_version != version:
|
||||
# this isn't us
|
||||
return
|
||||
|
||||
set_templates_button_disabled(true)
|
||||
ReleasesManager.export_templates_downloaded.disconnect(_on_export_templates_downloaded)
|
28
UI/Components/ReleaseItem.tscn
Normal file
28
UI/Components/ReleaseItem.tscn
Normal file
@ -0,0 +1,28 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://wpersx0asfqo"]
|
||||
|
||||
[ext_resource type="Script" path="res://UI/Components/ReleaseItem.gd" id="1_r5q2c"]
|
||||
|
||||
[node name="ReleaseItem" type="HBoxContainer"]
|
||||
offset_right = 512.0
|
||||
offset_bottom = 39.0
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("1_r5q2c")
|
||||
|
||||
[node name="VersionLabel" type="Label" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
alignment = 2
|
||||
|
||||
[node name="InstallButton" type="Button" parent="HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Install"
|
||||
|
||||
[node name="InstallTemplatesButton" type="Button" parent="HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Export Templates"
|
@ -21,12 +21,15 @@ func get_group_metadata() -> GroupMetadata:
|
||||
group_metadata.icon_path = icon_path_line_edit.text
|
||||
group_metadata.description = description_text_edit.text
|
||||
group_metadata.settings_overrides = override_editor_settings_menu.get_overrides()
|
||||
group_metadata.name = name_line_edit.text
|
||||
|
||||
group_metadata.save_request.emit()
|
||||
|
||||
return group_metadata
|
||||
|
||||
|
||||
func fill_from_group_metadata(p_name: String) -> void:
|
||||
name_line_edit.text = p_name
|
||||
func fill_from_group_metadata() -> void:
|
||||
name_line_edit.text = group_metadata.name
|
||||
icon_path_line_edit.text = group_metadata.icon_path
|
||||
description_text_edit.text = group_metadata.description
|
||||
override_editor_settings_menu.fill_from_dictionary(group_metadata.settings_overrides)
|
||||
|
@ -10,10 +10,10 @@ size = Vector2i(678, 419)
|
||||
script = ExtResource("1_ic3yh")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
offset_left = 12.0
|
||||
offset_top = 12.0
|
||||
offset_right = 666.0
|
||||
offset_bottom = 366.0
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 670.0
|
||||
offset_bottom = 370.0
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
53
UI/Dialogs/InstallEditDialog.gd
Normal file
53
UI/Dialogs/InstallEditDialog.gd
Normal file
@ -0,0 +1,53 @@
|
||||
extends ConfirmationDialog
|
||||
class_name InstallEditDialog
|
||||
|
||||
@onready var name_line_edit: LineEdit = %NameLineEdit
|
||||
@onready var icon_path_line_edit: LineEdit = %IconPathLineEdit
|
||||
@onready var open_icon_file_dialog_button: Button = %OpenIconFileDialogButton
|
||||
@onready var override_editor_settings_menu: OverrideEditorSettingsMenu = %OverrideEditorSettingsMenu
|
||||
@onready var version_selector: OptionButton = %VersionSelector
|
||||
|
||||
var install_metadata: InstallMetadata
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
name_line_edit.clear()
|
||||
icon_path_line_edit.clear()
|
||||
override_editor_settings_menu.clear()
|
||||
install_metadata = InstallMetadata.new()
|
||||
|
||||
version_selector.clear()
|
||||
for version in ReleasesManager.get_installed_versions():
|
||||
version_selector.add_item(version)
|
||||
version_selector.set_item_metadata(
|
||||
version_selector.get_item_count() - 1,
|
||||
ReleasesManager.get_version_metadata(version).binary_path)
|
||||
|
||||
|
||||
func get_install_metadata() -> InstallMetadata:
|
||||
install_metadata.name = name_line_edit.text
|
||||
install_metadata.icon_path = icon_path_line_edit.text
|
||||
install_metadata.local_overrides = override_editor_settings_menu.get_overrides()
|
||||
install_metadata.binary_path = version_selector.get_selected_metadata()
|
||||
|
||||
return install_metadata
|
||||
|
||||
|
||||
func fill_from_install_metadata() -> void:
|
||||
name_line_edit.text = install_metadata.name
|
||||
icon_path_line_edit.text = install_metadata.icon_path
|
||||
override_editor_settings_menu.fill_from_dictionary(install_metadata.local_overrides)
|
||||
var item_id: int = -1
|
||||
for i in version_selector.get_item_count():
|
||||
if version_selector.get_item_metadata(i) == install_metadata.binary_path:
|
||||
item_id = i
|
||||
break
|
||||
version_selector.select(item_id)
|
||||
|
||||
|
||||
func clear_ok_button_connections() -> void:
|
||||
for connection in get_signal_connection_list("confirmed"):
|
||||
var callable := connection["callable"] as Callable
|
||||
var _signal := connection["signal"] as Signal
|
||||
|
||||
_signal.disconnect(callable)
|
77
UI/Dialogs/InstallEditDialog.tscn
Normal file
77
UI/Dialogs/InstallEditDialog.tscn
Normal file
@ -0,0 +1,77 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://u0de32p0bb61"]
|
||||
|
||||
[ext_resource type="Script" path="res://UI/Dialogs/InstallEditDialog.gd" id="1_qfdve"]
|
||||
[ext_resource type="PackedScene" uid="uid://ig4wkr1lxvbv" path="res://UI/Components/OverrideEditorSettingsMenu/OverrideEditorSettingsMenu.tscn" id="2_elqgy"]
|
||||
|
||||
[node name="InstallEditDialog" type="ConfirmationDialog"]
|
||||
initial_position = 1
|
||||
title = "Edit Install"
|
||||
size = Vector2i(678, 419)
|
||||
script = ExtResource("1_qfdve")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 670.0
|
||||
offset_bottom = 370.0
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Name"
|
||||
|
||||
[node name="NameLineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Icon"
|
||||
|
||||
[node name="IconPathLineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Optional"
|
||||
|
||||
[node name="OpenIconFileDialogButton" type="Button" parent="VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "F"
|
||||
|
||||
[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer3"]
|
||||
layout_mode = 2
|
||||
text = "Godot Version"
|
||||
|
||||
[node name="VersionSelector" type="OptionButton" parent="VBoxContainer/HBoxContainer3"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 150)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="OverrideEditorSettingsMenu" parent="VBoxContainer/ScrollContainer" instance=ExtResource("2_elqgy")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/CenterContainer"]
|
||||
layout_mode = 2
|
||||
text = "Install overrides take precedence over group overrides."
|
||||
horizontal_alignment = 1
|
@ -16,6 +16,9 @@ func _ready() -> void:
|
||||
|
||||
group_edit_dialog.canceled.connect(group_edit_dialog.clear_ok_button_connections)
|
||||
|
||||
for gm in GroupsInstallsManager.get_all_group_metadatas():
|
||||
add_group_from_metadata(gm)
|
||||
|
||||
|
||||
func add_new_group() -> void:
|
||||
group_edit_dialog.clear()
|
||||
@ -25,14 +28,13 @@ func add_new_group() -> void:
|
||||
func():
|
||||
var gm := group_edit_dialog.get_group_metadata()
|
||||
|
||||
# TODO: hack, redo later
|
||||
var group_name := group_edit_dialog.name_line_edit.text
|
||||
|
||||
var group_menu: GroupMenu = GROUP_MENU_SCENE.instantiate()
|
||||
group_menus_container.add_child(group_menu)
|
||||
group_menu.setup_from_metadata(group_name, gm)
|
||||
group_menu.setup_from_metadata(gm)
|
||||
|
||||
group_menu.edit_group_button_pressed.connect(edit_group.bind(group_menu))
|
||||
GroupsInstallsManager.create_group_folder(gm)
|
||||
GroupsInstallsManager.group_metadata_should_save(gm)
|
||||
|
||||
, CONNECT_ONE_SHOT)
|
||||
|
||||
@ -43,13 +45,20 @@ func edit_group(group_menu: GroupMenu) -> void:
|
||||
group_edit_dialog.clear()
|
||||
group_edit_dialog.ok_button_text = "OK"
|
||||
|
||||
var group_name := group_menu.get_text()
|
||||
group_edit_dialog.group_metadata = group_menu.group_metadata
|
||||
group_edit_dialog.fill_from_group_metadata(group_name)
|
||||
group_edit_dialog.fill_from_group_metadata()
|
||||
|
||||
group_edit_dialog.confirmed.connect(
|
||||
func():
|
||||
group_menu.setup_from_metadata(group_name, group_edit_dialog.get_group_metadata())
|
||||
group_menu.setup_from_metadata(group_edit_dialog.get_group_metadata())
|
||||
, CONNECT_ONE_SHOT)
|
||||
|
||||
group_edit_dialog.show()
|
||||
|
||||
|
||||
func add_group_from_metadata(gm: GroupMetadata) -> void:
|
||||
var group_menu: GroupMenu = GROUP_MENU_SCENE.instantiate()
|
||||
group_menus_container.add_child(group_menu)
|
||||
group_menu.setup_from_metadata(gm)
|
||||
|
||||
group_menu.edit_group_button_pressed.connect(edit_group.bind(group_menu))
|
||||
|
27
UI/Main/Main.tscn
Normal file
27
UI/Main/Main.tscn
Normal file
@ -0,0 +1,27 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://0wv6qt367nml"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bf464y0q4l67h" path="res://UI/Main/GroupsView.tscn" id="1_qdj8e"]
|
||||
[ext_resource type="PackedScene" uid="uid://irfav51b5hf6" path="res://UI/Main/ReleasesView.tscn" id="2_sif55"]
|
||||
|
||||
[node name="Main" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="HSplitContainer" type="HSplitContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
split_offset = 400
|
||||
|
||||
[node name="ReleasesView" parent="HSplitContainer" instance=ExtResource("2_sif55")]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GroupsView" parent="HSplitContainer" instance=ExtResource("1_qdj8e")]
|
||||
layout_mode = 2
|
43
UI/Main/ReleasesView.gd
Normal file
43
UI/Main/ReleasesView.gd
Normal file
@ -0,0 +1,43 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const ITEM_SCENE := preload("res://UI/Components/ReleaseItem.tscn")
|
||||
|
||||
@onready var release_items_container: VBoxContainer = %ReleaseItemsContainer
|
||||
@onready var fetch_button: Button = %FetchButton
|
||||
@onready var status_label: Label = %StatusLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
ReleasesManager.releases_fetched.connect(releases_updated)
|
||||
ReleasesManager.releases_fetched.connect(func(_x):
|
||||
await get_tree().process_frame
|
||||
fetch_button.disabled = false
|
||||
)
|
||||
fetch_button.pressed.connect(func():
|
||||
ReleasesManager.fetch_releases()
|
||||
fetch_button.disabled = true
|
||||
)
|
||||
ReleasesManager.fetch_releases(true)
|
||||
|
||||
ReleasesManager.status_update.connect(status_label.set_text)
|
||||
|
||||
|
||||
func releases_updated(releases: ReleasesManager.Releases) -> void:
|
||||
clear_releases()
|
||||
for version in releases.releases:
|
||||
var item: ReleaseItem = ITEM_SCENE.instantiate()
|
||||
release_items_container.add_child(item)
|
||||
item.set_version(version)
|
||||
item.set_install_button_disabled(ReleasesManager.is_version_installed(version))
|
||||
item.set_templates_button_disabled(ReleasesManager.is_version_templates_installed(version))
|
||||
|
||||
item.install_button_pressed.connect(ReleasesManager.download_release)
|
||||
item.install_templates_button_pressed.connect(ReleasesManager.download_release)
|
||||
|
||||
ReleasesManager.version_downloaded.connect(item._on_version_downloaded)
|
||||
ReleasesManager.export_templates_downloaded.connect(item._on_export_templates_downloaded)
|
||||
|
||||
|
||||
func clear_releases() -> void:
|
||||
for i in release_items_container.get_children():
|
||||
i.queue_free()
|
50
UI/Main/ReleasesView.tscn
Normal file
50
UI/Main/ReleasesView.tscn
Normal file
@ -0,0 +1,50 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://irfav51b5hf6"]
|
||||
|
||||
[ext_resource type="Script" path="res://UI/Main/ReleasesView.gd" id="1_firao"]
|
||||
|
||||
[node name="ReleasesView" type="VBoxContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_firao")
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
text = "Godots"
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ReleaseItemsContainer" type="VBoxContainer" parent="PanelContainer/ScrollContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FetchButton" type="Button" parent="HBoxContainer/CenterContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Fetch"
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
alignment = 2
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="HBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Idle"
|
@ -11,10 +11,18 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="yagvm"
|
||||
run/main_scene="res://UI/Main/Main.tscn"
|
||||
config/use_custom_user_dir=true
|
||||
config/custom_user_dir_name="yagvm"
|
||||
config/features=PackedStringArray("4.0", "Forward Plus")
|
||||
run/low_processor_mode=true
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
DotEnv="*res://Classes/DotEnv.gd"
|
||||
ReleasesManager="*res://Classes/ReleasesManager.gd"
|
||||
GroupsInstallsManager="*res://Classes/GroupsInstallsManager.gd"
|
||||
|
||||
[display]
|
||||
|
||||
window/subwindows/embed_subwindows=false
|
||||
|
Reference in New Issue
Block a user