Make update UI more understandable (#446)

* Make update UI more understandable

Don't box the ui as the preferences are already part of a box (looks disgusting in 4.2)
Update to stable now on the same column as update to main to make them seem both equally useful
Change text for main/old to be more obviously important, from "Install main / old version" to "Install specific version (Latest or Older)"
Move the restore operator into another row

Updated readme with new instructions and screenshots

* Remove mat screenshot and link to updater instructions early into the readme

* Update redundant text

* merge request button

* Update README.md

Co-authored-by: Dragorn421 <[email protected]>

* change to gross github name only used by github and no one else

Co-authored-by: Dragorn421 <[email protected]>

* use to be added updater in fast64docs

* mention ability to test prs

---------

Co-authored-by: Dragorn421 <[email protected]>
This commit is contained in:
Lila
2025-05-20 14:47:30 +01:00
committed by GitHub
co-authored by Dragorn421
parent f2a0ca6330
commit 2a9a249ae7
8 changed files with 195 additions and 66 deletions
+2 -26
View File
@@ -8,13 +8,11 @@ Forked from [kurethedead/fast64 on BitBucket](https://bitbucket.org/kurethedead/
This is a Blender plugin that allows one to export F3D display lists. It also has the ability to export assets for Super Mario 64 and Ocarina of Time decompilation projects. It supports custom color combiners / geometry modes / etc. It is also possible to use exported C code in homebrew applications.
Make sure to save often, as this plugin is prone to crashing when creating materials / undoing material creation. This is a Blender issue.
<https://developer.blender.org/T70574>
Make sure to save often, as this plugin is prone to crashing when creating materials / undoing material creation. [This is a Blender issue](https://developer.blender.org/T70574).
### Example models can be found [here](https://github.com/Fast-64/fast64-models)
![alt-text](/images/mat_inspector.png)
Fast64 features an updater (which can also be used to try out in testing features and bugfixes), [follow these instructions to use it](https://fast64.readthedocs.io/en/latest/common/updater/updater.html)
### Credits
Thanks to anonymous_moose, Cheezepin, Rovert, and especially InTheBeef for testing.
@@ -63,28 +61,6 @@ Selecting F3DEX3 as your microcode unlocks a large number of additional presets
For cel shading, it is recommended to start with one of the cel shading presets, then modify the settings under the `Use Cel Shading` panel. Hover over each UI control for additional information about how that setting works.
### Updater
Fast64 features an updater ([CGCookie/blender-addon-updater](https://github.com/CGCookie/blender-addon-updater)).
It can be found in the addon preferences:
![How the updater in the addon preferences looks, right after addon install](/images/updater_initially.png)
Click the "Check now for fast64 update" button to check for updates.
![Updater preferences after clicking the "check for updates" button](/images/updater_after_check.png)
Click "Install main / old version" and choose "Main" if it isn't already selected:
![Updater: install main](/images/updater_install_main.png)
Click OK, there should be a message "Addon successfully installed" and prompting you to restart Blender:
![Updater: successful install, must restart](/images/updater_success_restart.png)
Clicking the red button will close Blender. After restarting, fast64 will be up-to-date with the latest main revision.
### Fast64 Development
If you'd like to develop in VSCode, follow this tutorial to get proper autocomplete. Skip the linter for now, we'll need to make sure the entire project gets linted before enabling autosave linting because the changes will be massive.
https://b3d.interplanety.org/en/using-microsoft-visual-studio-code-as-external-ide-for-writing-blender-scripts-add-ons/
+88 -3
View File
@@ -68,6 +68,8 @@ class SingletonUpdater:
self._latest_release = None
self._use_releases = False
self._include_branches = False
self._include_merge_requests = False
self._merge_requests = list()
self._include_branch_list = ['master']
self._include_branch_auto_check = False
self._manual_only = False
@@ -304,6 +306,17 @@ class SingletonUpdater:
except:
raise ValueError("include_branches must be a boolean value")
@property
def include_merge_requests(self):
return self._include_merge_requests
@include_merge_requests.setter
def include_merge_requests(self, value):
try:
self._include_merge_requests = bool(value)
except:
raise ValueError("include_branches must be a boolean value")
@property
def json(self):
if len(self._json) == 0:
@@ -415,6 +428,13 @@ class SingletonUpdater:
self._subfolder_path = value
@property
def merge_requests(self):
if len(self._merge_requests) == 0:
return {}
return {
mr["id"]: mr["title"] for mr in self._merge_requests
}
@property
def tags(self):
if len(self._tags) == 0:
return list()
@@ -590,9 +610,25 @@ class SingletonUpdater:
def form_tags_url(self):
return self._engine.form_tags_url(self)
def form_mrs_url(self):
return self._engine.form_mrs_url(self)
def form_branch_url(self, branch):
return self._engine.form_branch_url(branch, self)
def form_mr_url(self, mr):
return self._engine.form_mr_url(mr, self)
def get_mrs(self):
if self._include_merge_requests is False:
return
request = self.form_mrs_url()
self.print_verbose("Getting merge requests from server")
# get all merge requests, internet call
all_mrs = self._engine.parse_mrs(self.get_api(request), self) or []
self._merge_requests = [mr for mr in all_mrs if mr["state"] == "open"]
def get_tags(self):
request = self.form_tags_url()
@@ -1274,8 +1310,9 @@ class SingletonUpdater:
self._update_version,
self._update_link)
# Primary internet call, sets self._tags and self._tag_latest.
# Primary internet call, sets self._tags, self._merge_requests and self._tag_latest.
self.get_tags()
self.get_mrs()
self._json["last_check"] = str(datetime.now())
self.save_updater_json()
@@ -1357,8 +1394,21 @@ class SingletonUpdater:
self._update_link = link
if not tg:
raise ValueError("Version tag not found: " + name)
def set_mr(self, id: str):
"""Assign the merge request name and url to update to"""
mr = None
for merge_request in self._merge_requests:
if id == str(merge_request["id"]):
mr = merge_request
break
if mr:
self._update_link = self.form_mr_url(mr)
self._update_version = mr["id"]
else:
raise ValueError("Merge request not found: " + id)
def run_update(self, force=False, revert_tag=None, clean=False, callback=None):
def run_update(self, force=False, revert_tag=None, merge_request=False, clean=False, callback=None):
"""Runs an install, update, or reversion of an addon from online source
Arguments:
@@ -1372,7 +1422,10 @@ class SingletonUpdater:
self._json["version_text"] = dict()
if revert_tag is not None:
self.set_tag(revert_tag)
if merge_request:
self.set_mr(revert_tag)
else:
self.set_tag(revert_tag)
self._update_ready = True
# clear the errors if any
@@ -1643,9 +1696,15 @@ class BitbucketEngine:
def form_tags_url(self, updater):
return self.form_repo_url(updater) + "/refs/tags?sort=-name"
def form_mrs_url(self, updater):
raise NotImplementedError
def form_branch_url(self, branch, updater):
return self.get_zip_url(branch, updater)
def form_mr_url(self, mr, updater):
raise NotImplementedError
def get_zip_url(self, name, updater):
return "https://bitbucket.org/{user}/{repo}/get/{name}.zip".format(
@@ -1661,6 +1720,9 @@ class BitbucketEngine:
"name": tag["name"],
"zipball_url": self.get_zip_url(tag["name"], updater)
} for tag in response["values"]]
def parse_mrs(self, response, updater):
raise NotImplementedError
class GithubEngine:
@@ -1680,17 +1742,31 @@ class GithubEngine:
return "{}/releases".format(self.form_repo_url(updater))
else:
return "{}/tags".format(self.form_repo_url(updater))
def form_mrs_url(self, updater):
return "{}/pulls".format(self.form_repo_url(updater))
def form_branch_list_url(self, updater):
return "{}/branches".format(self.form_repo_url(updater))
def form_branch_url(self, branch, updater):
return "{}/zipball/{}".format(self.form_repo_url(updater), branch)
def form_mr_url(self, mr, updater):
head = mr["head"]
branch_name = head["ref"]
repo = head["repo"]["url"]
return "{}/zipball/{}".format(repo, branch_name)
def parse_tags(self, response, updater):
if response is None:
return list()
return response
def parse_mrs(self, response, updater):
if response is None:
return list()
return response
class GitlabEngine:
@@ -1706,6 +1782,9 @@ class GitlabEngine:
def form_tags_url(self, updater):
return "{}/repository/tags".format(self.form_repo_url(updater))
def form_mrs_url(self, updater):
raise NotImplementedError
def form_branch_list_url(self, updater):
# does not validate branch name.
@@ -1717,6 +1796,9 @@ class GitlabEngine:
# instead of branch zip to get direct path, would need.
return "{}/repository/archive.zip?sha={}".format(
self.form_repo_url(updater), branch)
def form_mr_url(self, mr, updater):
raise NotImplementedError
def get_zip_url(self, sha, updater):
return "{base}/repository/archive.zip?sha={sha}".format(
@@ -1734,6 +1816,9 @@ class GitlabEngine:
"name": tag["name"],
"zipball_url": self.get_zip_url(tag["commit"]["id"], updater)
} for tag in response]
def parse_mrs(self, response, updater):
raise NotImplementedError
# -----------------------------------------------------------------------------
+105 -37
View File
@@ -58,7 +58,7 @@ except Exception as e:
self.error_msg = None
self.async_checking = None
def run_update(self, force, callback, clean):
def run_update(self, force, callback, merge_request, clean):
pass
def check_for_update(self, now):
@@ -340,14 +340,8 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator):
def target_version(self, context):
# In case of error importing updater.
if updater.invalid_updater:
ret = []
ret = []
i = 0
for tag in updater.tags:
ret.append((tag, tag, "Select to install " + tag))
i += 1
return ret
return []
return [(tag, tag, "Select to install " + tag) for tag in updater.tags]
target = bpy.props.EnumProperty(
name="Target version to install",
@@ -407,6 +401,78 @@ class AddonUpdaterUpdateTarget(bpy.types.Operator):
return {'FINISHED'}
class AddonUpdaterTryMR(bpy.types.Operator):
bl_label = updater.addon + " merge requests"
bl_idname = updater.addon + ".updater_try_merge_request"
bl_description = "Install a merge request of the {x} addon".format(
x=updater.addon)
bl_options = {'REGISTER', 'INTERNAL'}
def target_version(self, context):
# In case of error importing updater.
if updater.invalid_updater:
return []
return [(str(id), title, "Select to install " + title) for id, title in updater.merge_requests.items()]
target = bpy.props.EnumProperty(
name="Target version to install",
description="Select the version to install",
items=target_version
)
# If true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains.
clean_install = bpy.props.BoolProperty(
name="Clean install",
description=("If enabled, completely clear the addon's folder before "
"installing new update, creating a fresh install"),
default=False,
options={'HIDDEN'}
)
@classmethod
def poll(cls, context):
if updater.invalid_updater:
return False
return updater.update_ready is not None and len(updater.merge_requests) > 0
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
if updater.invalid_updater:
layout.label(text="Updater error")
return
split = layout_split(layout, factor=0.5)
sub_col = split.column()
sub_col.label(text="Select install version")
sub_col = split.column()
sub_col.prop(self, "target", text="")
def execute(self, context):
# In case of error importing updater.
if updater.invalid_updater:
return {'CANCELLED'}
res = updater.run_update(
force=False,
revert_tag=self.target,
merge_request=True,
callback=post_update_callback,
clean=self.clean_install)
# Should return 0, if not something happened.
if res == 0:
updater.print_verbose("Updater returned successful")
else:
updater.print_verbose(
"Updater returned {}, , error occurred".format(res))
return {'CANCELLED'}
return {'FINISHED'}
class AddonUpdaterInstallManually(bpy.types.Operator):
"""As a fallback, direct the user to download the addon manually"""
@@ -950,21 +1016,20 @@ def update_settings_ui(self, context, element=None):
# Element is a UI element, such as layout, a row, column, or box.
if element is None:
element = self.layout
box = element.box()
# In case of error importing updater.
if updater.invalid_updater:
box.label(text="Error initializing updater code:")
box.label(text=updater.error_msg)
element.label(text="Error initializing updater code:")
element.label(text=updater.error_msg)
return
settings = get_user_preferences(context)
if not settings:
box.label(text="Error getting updater preferences", icon='ERROR')
element.label(text="Error getting updater preferences", icon='ERROR')
return
# auto-update settings
box.label(text="Updater Settings")
row = box.row()
element.label(text="Updater Settings")
row = element.row()
# special case to tell user to restart blender, if set that way
if not updater.auto_reload_post_update:
@@ -976,7 +1041,7 @@ def update_settings_ui(self, context, element=None):
icon="ERROR")
return
split = layout_split(row, factor=0.4)
split = layout_split(row, factor=0.5)
sub_col = split.column()
sub_col.prop(settings, "auto_check_update")
sub_col = split.column()
@@ -998,7 +1063,7 @@ def update_settings_ui(self, context, element=None):
# check_col.prop(settings,"updater_interval_minutes")
# Checking / managing updates.
row = box.row()
row = element.row()
col = row.column()
if updater.error is not None:
sub_col = col.row(align=True)
@@ -1018,7 +1083,7 @@ def update_settings_ui(self, context, element=None):
split.operator(AddonUpdaterCheckNow.bl_idname,
text="", icon="FILE_REFRESH")
elif updater.update_ready is None and not updater.async_checking:
elif not updater.async_checking:
col.scale_y = 2
col.operator(AddonUpdaterCheckNow.bl_idname)
elif updater.update_ready is None: # async is running
@@ -1048,25 +1113,13 @@ def update_settings_ui(self, context, element=None):
split.operator(AddonUpdaterCheckNow.bl_idname,
text="", icon="FILE_REFRESH")
elif updater.update_ready and not updater.manual_only:
sub_col = col.row(align=True)
sub_col.scale_y = 1
split = sub_col.split(align=True)
split.scale_y = 2
split.operator(AddonUpdaterUpdateNow.bl_idname,
text="Update now to " + str(updater.update_version))
split = sub_col.split(align=True)
split.scale_y = 2
split.operator(AddonUpdaterCheckNow.bl_idname,
text="", icon="FILE_REFRESH")
elif updater.update_ready and updater.manual_only:
col.scale_y = 2
dl_now_txt = "Download " + str(updater.update_version)
col.operator("wm.url_open",
text=dl_now_txt).url = updater.website
else: # i.e. that updater.update_ready == False.
sub_col = col.row(align=True)
else: # i.e. that updater.update_ready == False.
sub_col = element.row(align=True)
sub_col.scale_y = 1
split = sub_col.split(align=True)
split.enabled = False
@@ -1080,13 +1133,24 @@ def update_settings_ui(self, context, element=None):
if not updater.manual_only:
col = row.column(align=True)
if updater.update_ready:
sub_col = col.row(align=True)
sub_col.scale_y = 1
split = sub_col.split(align=True)
split.operator(AddonUpdaterUpdateNow.bl_idname,
text=f"Update now to {updater.update_version} (Stable)")
if updater.include_branches and len(updater.include_branch_list) > 0:
branch = updater.include_branch_list[0]
col.operator(AddonUpdaterUpdateTarget.bl_idname,
text="Install {} / old version".format(branch))
text="Install specific version (Latest or Older)")
else:
col.operator(AddonUpdaterUpdateTarget.bl_idname,
text="(Re)install addon version")
if updater.include_merge_requests and len(updater.merge_requests) > 0:
col.operator(AddonUpdaterTryMR.bl_idname,
text="Try a pull request")
last_date = "none found"
backup_path = os.path.join(updater.stage_path, "backup")
if "backup_date" in updater.json and os.path.isdir(backup_path):
@@ -1095,9 +1159,9 @@ def update_settings_ui(self, context, element=None):
else:
last_date = updater.json["backup_date"]
backup_text = "Restore addon backup ({})".format(last_date)
col.operator(AddonUpdaterRestoreBackup.bl_idname, text=backup_text)
element.operator(AddonUpdaterRestoreBackup.bl_idname, text=backup_text)
row = box.row()
row = element.row()
row.scale_y = 0.7
last_check = updater.json["last_check"]
if updater.error is not None and updater.error_msg is not None:
@@ -1195,7 +1259,7 @@ def update_settings_ui_condensed(self, context, element=None):
split = sub_col.split(align=True)
split.scale_y = 2
split.operator(AddonUpdaterUpdateNow.bl_idname,
text="Update now to " + str(updater.update_version))
text=f"Update now to {updater.update_version} (Stable)")
split = sub_col.split(align=True)
split.scale_y = 2
split.operator(AddonUpdaterCheckNow.bl_idname,
@@ -1358,6 +1422,7 @@ classes = (
AddonUpdaterCheckNow,
AddonUpdaterUpdateNow,
AddonUpdaterUpdateTarget,
AddonUpdaterTryMR,
AddonUpdaterInstallManually,
AddonUpdaterUpdatedSuccessful,
AddonUpdaterRestoreBackup,
@@ -1440,6 +1505,9 @@ def register(bl_info):
# the "install {branch}/older version" operator.
updater.include_branches = True
# Allows the user to try merge requests as an option to "update" to.
updater.include_merge_requests = True
# (GitHub only) This options allows using "releases" instead of "tags",
# which enables pulling down release logs/notes, as well as installs update
# from release-attached zips (instead of the auto-packaged code generated
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB