From 8b9f751ff1852713dc9da9edf45e27e36b94045d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:49:59 +0900 Subject: [PATCH] fix: error handling for no local mods --- CHANGELOG.md | 8 +++++ dzgui.sh | 8 +++-- helpers/funcs | 57 ++++++++++++++++++++++++---------- helpers/ui.py | 85 ++++++++++++++++++++++++++++++++------------------- 4 files changed, 108 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daeff58..cd3f24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [5.6.0-beta.3] 2024-11-18 +### Fixed +- Improved handling for cases where there are no locally installed mods +- Set up mod symlinks at boot, rather than only on server connect +- Prevent context menus from opening when table is empty +- When reloading table in-place, prevent duplicate panel elements from being added if already present +- Clean up signal emission + ## [5.6.0-beta.2] 2024-11-15 ### Fixed - Clean up local mod signatures from versions file when deleting mods diff --git a/dzgui.sh b/dzgui.sh index 5af16f5..7f1ab2f 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -569,10 +569,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="680ff0e4071681f26409fa3592a41e46" + ["ui.py"]="4663cdda7bf91a0c594103d6f4382f15" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="acd5d85b27141082b25e07138f8b5b54" + ["funcs"]="5eae515ea2cac2ab38212a529415e86b" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" @@ -874,6 +874,9 @@ stale_mod_signatures(){ fi } +create_new_links(){ + "$HOME/.local/share/$app_name/helpers/funcs" "update_symlinks" +} initial_setup(){ setup_dirs setup_state_files @@ -895,6 +898,7 @@ initial_setup(){ migrate_files stale_symlinks stale_mod_signatures + create_new_links local_latlon is_steam_running is_dzg_downloading diff --git a/helpers/funcs b/helpers/funcs index 0375292..09037f6 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -116,7 +116,7 @@ declare -A funcs=( ["query_config"]="query_config" ["start_cooldown"]="start_cooldown" ["list_mods"]="list_mods" -["delete"]="delete_local_mod" +["Delete selected mods"]="delete_local_mod" ["align_local"]="align_versions_file" ["show_server_modlist"]="show_server_modlist" ["test_ping"]="test_ping" @@ -131,6 +131,7 @@ declare -A funcs=( ["Handshake"]="final_handshake" ["get_player_count"]="get_player_count" ["lan_scan"]="lan_scan" +["update_symlinks"]="update_symlinks" ) lan_scan(){ @@ -588,19 +589,34 @@ align_versions_file(){ mv $versions_file.new $versions_file logger INFO "Removed local signatures for the mod '$mod'" } +pluralize(){ + local plural="$1" + local count="$2" + local mod + local suffix + local base + local ct + local s + + if [[ "${plural: -2}" == "es" ]]; then + base="${plural::-2}" + suffix="${plural: -2}" + ct=$((count^2)) + [[ $ct -ne 3 ]] && s="$suffix" + else + base="${plural::-1}" + suffix="${plural: -1}" + ct=$((count^1)) + [[ $ct -gt 0 ]] && s="$suffix" + fi + + printf "%s%s" "$base" "$s" +} delete_local_mod(){ shift - if [[ -z $1 ]]; then - # use multi mode - readarray -t symlinks < <(awk '{print $1}' $_cache_mods_temp) - readarray -t ids < <(awk '{print $2}' $_cache_mods_temp) - rm "$_cache_mods_temp" - else - local symlink="$1" - local dir="$2" - readarray -t symlinks <<< "$symlink" - readarray -t ids <<< "$dir" - fi + readarray -t symlinks < <(awk '{print $1}' $_cache_mods_temp) + readarray -t ids < <(awk '{print $2}' $_cache_mods_temp) + rm "$_cache_mods_temp" for ((i=0; i<${#symlinks[@]}; i++)); do [[ ! -d $workshop_dir/${ids[$i]} ]] && return 1 [[ ! -L $game_dir/${symlinks[$i]} ]] && return 1 @@ -608,16 +624,17 @@ delete_local_mod(){ rm -rf "${workshop_dir:?}/${ids[$i]}" && unlink "$game_dir/${symlinks[$i]}" || return 1 align_versions_file "align" "${ids[$i]}" done + printf "Successfully deleted %s %s." "${#symlinks[@]}" "$(pluralize "mods" ${#symlinks[@]})" + return 95 } test_cooldown(){ [[ ! -f $_cache_cooldown ]] && return 0 local old_time=$(< $_cache_cooldown) local cur_time=$(date +%s) local delta=$(($cur_time - $old_time)) - local suffix="seconds" if [[ $delta -lt 60 ]]; then local remains=$((60 - $delta)) - [[ $remains -eq 1 ]] && suffix="second" + local suffix=$(pluralize "seconds" $remains) printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix" exit 1 fi @@ -1104,7 +1121,11 @@ legacy_symlinks(){ unlink "$d" fi done - for d in "$workshop_dir"/*; do + readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) + [[ ${#mod_dirs[@]} -eq 0 ]] && return + for d in "${mod_dirs[@]}"; do + # suppress errors if mods are downloading at boot + [[ ! -f "$d/meta.cpp" ]] && continue local id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') local encoded_id=$(echo "$id" | awk '{printf("%c",$1)}' | base64 | sed 's/\//_/g; s/=//g; s/+/]/g') if [[ -h "$game_dir/@$encoded_id" ]]; then @@ -1113,7 +1134,11 @@ legacy_symlinks(){ done } symlinks(){ - for d in "$workshop_dir"/*; do + readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) + [[ ${#mod_dirs[@]} -eq 0 ]] && return + for d in "${mod_dirs[@]}"; do + # suppress errors if mods are downloading at boot + [[ ! -f "$d/meta.cpp" ]] && continue id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') encoded_id=$(encode "$id") link="@$encoded_id" diff --git a/helpers/ui.py b/helpers/ui.py index 16cd43e..a6ba4ea 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -330,8 +330,8 @@ def spawn_dialog(transient_parent, msg, mode): def process_shell_return_code(transient_parent, msg, code, original_input): + logger.info("Processing return code '%s' for the input '%s', returned message '%s'" %(code, original_input, msg)) match code: - #TODO: add logger output to each case 0: # success with notice popup spawn_dialog(transient_parent, msg, Popup.NOTIFY) @@ -369,6 +369,13 @@ def process_shell_return_code(transient_parent, msg, code, original_input): transient_parent.grid.update_statusbar(tooltip) spawn_dialog(transient_parent, msg, Popup.NOTIFY) return + case 95: + # reload mods list + spawn_dialog(transient_parent, msg, Popup.NOTIFY) + treeview = transient_parent.grid.scrollable_treelist.treeview + # re-block this signal before redrawing table contents + toggle_signal(treeview, treeview, '_on_keypress', False) + treeview.update_quad_column("List installed mods") case 100: # final handoff before launch final_conf = spawn_dialog(transient_parent, msg, Popup.CONFIRM) @@ -427,6 +434,8 @@ def process_tree_option(input, treeview): else: # non-blocking subprocess subprocess.Popen(['/usr/bin/env', 'bash', funcs, "Open link", command]) + case "Delete selected mods": + call_on_thread(True, context, "Deleting mods", command) case "Handshake": call_on_thread(True, context, "Waiting for DayZ", command) case _: @@ -605,7 +614,9 @@ class ButtonBox(Gtk.Box): # only applicable when returning from mod list grid = widgets["grid"] - grid.right_panel.remove(grid.sel_panel) + grid_last_child = grid.right_panel.get_children()[-1] + if isinstance(grid_last_child, ModSelectionPanel): + grid.right_panel.remove(grid.sel_panel) right_panel = self.get_parent() right_panel.set_filter_visibility(False) @@ -780,15 +791,15 @@ class TreeView(Gtk.TreeView): success_msg = "Successfully deleted the mod '%s'." %(value) fail_msg = "An error occurred during deletion. Aborting." res = spawn_dialog(parent, conf_msg, Popup.CONFIRM) - symlink = self.get_column_at_index(1) - dir = self.get_column_at_index(2) if res == 0: - proc = call_out(parent, "delete", symlink, dir) - if proc.returncode == 0: - spawn_dialog(parent, success_msg, Popup.NOTIFY) - self.update_quad_column("List installed mods") - else: - spawn_dialog(parent, fail_msg, Popup.NOTIFY) + mods = [] + symlink = self.get_column_at_index(1) + dir = self.get_column_at_index(2) + concat = symlink + " " + dir + "\n" + mods.append(concat) + with open(mods_temp_file, "w") as outfile: + outfile.writelines(mods) + process_tree_option(["Delete selected mods", ""], self) case "Open in Steam Workshop": record = self.get_column_at_index(2) call_out(parent, "open_workshop_page", record) @@ -851,6 +862,11 @@ class TreeView(Gtk.TreeView): self.menu.show_all() if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l: + sel = self.get_selection() + sels = sel.get_selected_rows() + (model, pathlist) = sels + if len(pathlist) < 1: + return self.menu.popup_at_widget(widget, Gdk.Gravity.CENTER, Gdk.Gravity.WEST) else: self.menu.popup_at_pointer(event) @@ -973,8 +989,11 @@ class TreeView(Gtk.TreeView): def _focus_first_row(self): path = Gtk.TreePath(0) - it = mod_store.get_iter(path) - self.get_selection().select_path(path) + try: + it = mod_store.get_iter(path) + self.get_selection().select_path(path) + except ValueError: + pass def get_column_at_index(self, index): select = self.get_selection() @@ -1056,31 +1075,42 @@ class TreeView(Gtk.TreeView): def _background_quad(self, dialog, mode): def load(): dialog.destroy() + # detach button panel if store is empty + if isinstance(panel_last_child, ModSelectionPanel): + if total_mods == 0: + right_panel.remove(grid.sel_panel) + grid.show_all() + right_panel.set_filter_visibility(False) self.set_model(mod_store) self.grab_focus() - if abort is False: - size = locale.format_string('%.3f', total_size, grouping=True) - pretty = pluralize("mods", total_mods) - grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") + size = locale.format_string('%.3f', total_size, grouping=True) + pretty = pluralize("mods", total_mods) + grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") #2024-11-12 toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) toggle_signal(self, self, '_on_keypress', True) self._focus_first_row() + if total_mods == 0: + spawn_dialog(self.get_outer_window(), data.stdout, Popup.NOTIFY) - grid = self.get_outer_grid() + widgets = relative_widget(self) + grid = widgets["grid"] right_panel = grid.right_panel - - abort = False - right_panel.set_filter_visibility(False) data = call_out(self, "list_mods", mode) + panel_last_child = right_panel.get_children()[-1] # suppress errors if no mods available on system if data.returncode == 1: - abort = True + total_mods = 0 + total_size = 0 GLib.idle_add(load) - spawn_dialog(self.get_outer_window(), data.stdout, Popup.NOTIFY) return 1 + # attach button panel only if missing (prevents duplication when reloading in-place) + if not isinstance(panel_last_child, ModSelectionPanel): + right_panel.pack_start(grid.sel_panel, False, False, 0) + grid.show_all() + right_panel.set_filter_visibility(False) result = parse_mod_rows(data) total_size = result[0] total_mods = result[1] @@ -1214,10 +1244,6 @@ class TreeView(Gtk.TreeView): if mode == "List installed mods": cols = mod_cols self.set_model(mod_store) - # attach button panel - grid = self.get_parent().get_parent() - grid.right_panel.pack_start(grid.sel_panel, False, False, 0) - grid.show_all() else: cols = log_cols self.set_model(log_store) @@ -1988,12 +2014,7 @@ class ModSelectionPanel(Gtk.Box): mods.append(concat) with open(mods_temp_file, "w") as outfile: outfile.writelines(mods) - proc = call_out(parent, "delete") - if proc.returncode == 0: - spawn_dialog(parent, success_msg, Popup.NOTIFY) - treeview.update_quad_column("List installed mods") - else: - spawn_dialog(parent, fail_msg, Popup.NOTIFY) + process_tree_option(["Delete selected mods", ""], treeview) class FilterPanel(Gtk.Box): def __init__(self):