From e7f4416f369dc7378d8ec28bbeee3b5099cb85e1 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Sun, 9 Mar 2025 15:31:40 -0400 Subject: [PATCH 01/52] Handle a broken symlink for internal logging folder --- functions/global.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/functions/global.sh b/functions/global.sh index a1aee4b8..601ac658 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -8,6 +8,11 @@ : "${logging_level:=info}" # Initializing the log level variable if not already valued, this will be actually red later from the config file rd_logs_folder="/var/config/retrodeck/logs" # Static location to write all RetroDECK-related logs +if [ -h "$rd_logs_folder" ]; then # Check if internal logging folder is already a symlink + if [ ! -e "$rd_logs_folder" ]; then # Check if internal logging folder symlink is broken + unlink "$rd_logs_folder" # Remove broken symlink so the folder is recreated when sourcing logger.sh + fi +fi source /app/libexec/logger.sh rotate_logs From bebb84469fd68f0685608d562b13b486f324d027 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Sun, 9 Mar 2025 15:32:37 -0400 Subject: [PATCH 02/52] Revove unneeded variable declaration in global.sh --- functions/global.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/functions/global.sh b/functions/global.sh index 601ac658..8345b2ff 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -241,7 +241,6 @@ else # Verify rdhome is where it is supposed to be. if [[ ! -d "$rdhome" ]]; then - prev_home_path="$rdhome" configurator_generic_dialog "RetroDECK Setup" "The RetroDECK data folder was not found in the expected location.\nThis may happen when SteamOS is updated.\n\nPlease browse to the current location of the \"retrodeck\" folder." new_home_path=$(directory_browse "RetroDECK folder location") set_setting_value $rd_conf "rdhome" "$new_home_path" retrodeck "paths" From 67a77e9932b7f0d146f85b9fc9be9e84f22a1099 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Sun, 9 Mar 2025 15:33:13 -0400 Subject: [PATCH 03/52] Fix incorrect ordering of prepare_component call after finding lost rdhome location --- functions/global.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/global.sh b/functions/global.sh index 8345b2ff..90c8519b 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -246,8 +246,8 @@ else set_setting_value $rd_conf "rdhome" "$new_home_path" retrodeck "paths" conf_read #tmplog_merger # This function is tempry(?) removed - prepare_component "retrodeck" "postmove" - prepare_component "all" "postmove" + prepare_component "postmove" "retrodeck" + prepare_component "postmove" "all" conf_write fi From 9838f55c83fb27544559857a5f8cba61afbdb02e Mon Sep 17 00:00:00 2001 From: Adam Iannazzone <46025473+jiannazzone@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:49:23 -0400 Subject: [PATCH 04/52] Create icon-ponzu.svg --- res/icon-ponzu.svg | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 res/icon-ponzu.svg diff --git a/res/icon-ponzu.svg b/res/icon-ponzu.svg new file mode 100644 index 00000000..483a36d0 --- /dev/null +++ b/res/icon-ponzu.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + From 9cfb63304eaf28a17100e283bfedf6154b2606bd Mon Sep 17 00:00:00 2001 From: Adam Iannazzone <46025473+jiannazzone@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:52:32 -0400 Subject: [PATCH 05/52] Update quality of SVG rd icons --- res/Affinity Files/icon.afphoto | Bin 60390 -> 83479 bytes res/icon-configurator.svg | 24 +++++++++++------------- res/icon-engine.svg | 20 ++++++++++++-------- res/icon-framework.svg | 20 ++++++++++---------- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/res/Affinity Files/icon.afphoto b/res/Affinity Files/icon.afphoto index b88cb5ff897500772e91dadaa548069758afdba6..14c8ea8a01c30c9c038a034ffcb8d3e4ad0243e4 100644 GIT binary patch delta 68653 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 G!v7DAt!2aj delta 45517 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 diff --git a/res/icon-configurator.svg b/res/icon-configurator.svg index 5df3161b..d6f515ea 100644 --- a/res/icon-configurator.svg +++ b/res/icon-configurator.svg @@ -1,20 +1,18 @@ - - - - - - - - - - - - + + + + + + + + + + - + diff --git a/res/icon-engine.svg b/res/icon-engine.svg index 30db9cf8..bb776009 100644 --- a/res/icon-engine.svg +++ b/res/icon-engine.svg @@ -1,14 +1,18 @@ - - - - - - - + + + + + + + + + + + - + diff --git a/res/icon-framework.svg b/res/icon-framework.svg index 95fbb2c6..95ced5b1 100644 --- a/res/icon-framework.svg +++ b/res/icon-framework.svg @@ -1,18 +1,18 @@ - - - - - - - + + + + - - + + + + + - + From fea3a3e3c2eaa80d38a48f3c8a7e34bf0294258d Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 13 Mar 2025 14:53:05 -0400 Subject: [PATCH 06/52] Add repair_paths function to fix paths that cannot be found without needing a full reset --- functions/other_functions.sh | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/functions/other_functions.sh b/functions/other_functions.sh index d78f30ec..e8231f83 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -1128,3 +1128,50 @@ add_retrodeck_to_steam(){ log i "RetroDECK has been added to Steam" } + +repair_paths() { + # This function will verify that all folders defined in the [paths] section of retrodeck.cfg exist + # If a folder doesn't exist and is defined outside of rdhome, it will check in rdhome first and have the user browse for them manually if it isn't there either + # USAGE: repair_paths + + invalid_path_found="false" + + log i "Checking that all RetroDECK paths are valid" + while read -r config_line; do + local current_setting_name=$(get_setting_name "$config_line" "retrodeck") + if [[ ! $current_setting_name =~ (rdhome|sdcard) ]]; then # Ignore these locations + local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "paths") + if [[ ! -d "$current_setting_value" ]]; then # If the folder doesn't exist as defined + log i "$current_setting_name does not exist as defined, config is incorrect" + if [[ ! -d "$rdhome/${current_setting_value#*retrodeck/}" ]]; then # If the folder doesn't exist within defined rdhome path + if [[ ! -d "$sdcard/${current_setting_value#*retrodeck/}" ]]; then # If the folder doesn't exist within defined sdcard path + log i "$current_setting_name cannot be found at any expected location, having user locate it manually" + configurator_generic_dialog "RetroDECK Path Repair" "The RetroDECK $current_setting_name was not found in the expected location.\nThis may happen when the folder is moved manually.\n\nPlease browse to the current location of the $current_setting_name." + new_path=$(directory_browse "RetroDECK $current_setting_name location") + set_setting_value "$rd_conf" "$current_setting_name" "$new_path" retrodeck "paths" + invalid_path_found="true" + else # Folder does exist within defined sdcard path, update accordingly + log i "$current_setting_name found in $sdcard/retrodeck, correcting path config" + new_path="$sdcard/retrodeck/${current_setting_value#*retrodeck/}" + set_setting_value "$rd_conf" "$current_setting_name" "$new_path" retrodeck "paths" + invalid_path_found="true" + fi + else # Folder does exist within defined rdhome path, update accordingly + log i "$current_setting_name found in $rdhome, correcting path config" + new_path="$rdhome/${current_setting_value#*retrodeck/}" + set_setting_value "$rd_conf" "$current_setting_name" "$new_path" retrodeck "paths" + invalid_path_found="true" + fi + fi + fi + done < <(grep -v '^\s*$' $rd_conf | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f') + + if [[ $invalid_path_found == "true" ]]; then + log i "One or more invalid paths repaired, fixing internal RetroDECK structures" + conf_read + dir_prep "$logs_folder" "$rd_logs_folder" + prepare_component "postmove" "all" + else + log i "All folders were found at their expected locations" + fi +} From 96e088e7238df09fbd5361c78c61c193f712f050 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 13 Mar 2025 14:54:05 -0400 Subject: [PATCH 07/52] Add repair-paths CLI option --- retrodeck.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/retrodeck.sh b/retrodeck.sh index 8b24fe3c..75a60005 100644 --- a/retrodeck.sh +++ b/retrodeck.sh @@ -109,6 +109,10 @@ while [[ $# -gt 0 ]]; do cli_compress_all_games "$2" shift 2 ;; + --repair-paths) + repair_paths + exit 0 + ;; --configurator) sh /app/tools/configurator.sh exit 0 From 9b6bcd1ed7a2756f6a872f0bfcd5f4061fc38a7b Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 13 Mar 2025 15:00:19 -0400 Subject: [PATCH 08/52] Add Repair RetroDECK Paths option to Configurator --- tools/configurator.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/configurator.sh b/tools/configurator.sh index a8f8dca7..36088d78 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -76,6 +76,7 @@ rd_zenity --progress --no-cancel --pulsate --auto-close \ # - Update Notification # - Add RetroDECK to Steam # - M3U Multi-File Validator +# - Repair RetroDECK paths # - Ponzu: Remove Yuzu # - Ponzu: Remove Citra # - Steam Sync @@ -443,6 +444,7 @@ configurator_tools_dialog() { "Update Notification" "Enable / Disable: Notifications for new RetroDECK versions." "Add RetroDECK to Steam" "Add RetroDECK shortcut to Steam. Steam restart required." "M3U Multi-File Validator" "Verify the proper structure of multi-file or multi-disc games." + "Repair RetroDECK Paths" "Repair RetroDECK folder path configs for unexpectedly missing folders." ) if [[ $(get_setting_value "$rd_conf" "kiroi_ponzu" "retrodeck" "options") == "true" ]]; then @@ -550,6 +552,12 @@ configurator_tools_dialog() { configurator_check_multifile_game_structure ;; + "Repair RetroDECK Paths" ) + log i "Configurator: opening \"$choice\" menu" + repair_paths + configurator_tools_dialog + ;; + "Ponzu: Remove Yuzu" ) ponzu_remove "yuzu" ;; From 953d6821519f4d215a2ec8cbb1eb5314c893af5e Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 13 Mar 2025 15:05:00 -0400 Subject: [PATCH 09/52] Fix some incorrect/missing Configurator menu navigation --- functions/configurator_functions.sh | 1 - tools/configurator.sh | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/configurator_functions.sh b/functions/configurator_functions.sh index acf7ff5a..2d68829c 100644 --- a/functions/configurator_functions.sh +++ b/functions/configurator_functions.sh @@ -61,5 +61,4 @@ configurator_check_multifile_game_structure() { else configurator_generic_dialog "RetroDECK Configurator - Verify Multi-file Structure" "No incorrect multi-file game folder structures found." fi - configurator_welcome_dialog } diff --git a/tools/configurator.sh b/tools/configurator.sh index 36088d78..7892d172 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -476,12 +476,13 @@ configurator_tools_dialog() { else configurator_generic_dialog "RetroDECK Configurator - Backup Userdata" "The backup process could not be completed,\nplease check the logs folder for more information." fi - configurator_welcome_dialog + configurator_tools_dialog ;; "BIOS Checker" ) log i "Configurator: opening \"$choice\" menu" configurator_bios_checker + configurator_tools_dialog ;; "Games Compressor" ) @@ -545,11 +546,13 @@ configurator_tools_dialog() { "Add RetroDECK to Steam" ) add_retrodeck_to_steam + configurator_tools_dialog ;; "M3U Multi-File Validator" ) log i "Configurator: opening \"$choice\" menu" configurator_check_multifile_game_structure + configurator_tools_dialog ;; "Repair RetroDECK Paths" ) @@ -1074,8 +1077,6 @@ configurator_bios_checker() { --title "RetroDECK Configurator Utility - BIOS Check in Progress" \ --text="This check will look for BIOS files that RetroDECK has identified as working.\n\nNot all BIOS files are required for games to work, please check the BIOS description for more information on its purpose.\n\nBIOS files not known to this tool could still function.\n\nSome more advanced emulators such as Ryujinx will have additional methods to verify that the BIOS files are in working order.\n\nRetroDECK is now checking your BIOS files, please wait...\n\n" \ --width=400 --height=100 - - configurator_welcome_dialog } configurator_reset_dialog() { From ee34451ed6ce2c30c8b19b2239b8a0100e71edbc Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 13 Mar 2025 15:07:03 -0400 Subject: [PATCH 10/52] Add GUI final feedback to path repair process --- functions/other_functions.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/functions/other_functions.sh b/functions/other_functions.sh index e8231f83..e1e4cc15 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -1171,7 +1171,9 @@ repair_paths() { conf_read dir_prep "$logs_folder" "$rd_logs_folder" prepare_component "postmove" "all" + configurator_generic_dialog "RetroDECK Path Repair" "One or more incorrectly configured paths were repaired." else log i "All folders were found at their expected locations" + configurator_generic_dialog "RetroDECK Path Repair" "All RetroDECK folders were found at their expected locations." fi } From 7d3d058050108006b7dd70ea634f0d78cdd861ce Mon Sep 17 00:00:00 2001 From: icenine451 Date: Fri, 14 Mar 2025 08:42:01 -0400 Subject: [PATCH 11/52] Update cooker to 0.9.2b in manifest --- net.retrodeck.retrodeck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net.retrodeck.retrodeck.yml b/net.retrodeck.retrodeck.yml index fe4a25ac..13d0f819 100644 --- a/net.retrodeck.retrodeck.yml +++ b/net.retrodeck.retrodeck.yml @@ -86,7 +86,7 @@ modules: # VERSION INITIALIZATION # on main please update this with the version variable, eg: VERSION=0.8.0b # on cooker will be VERSION=cooker-0.9.0b for example - VERSION=cooker-0.9.1b + VERSION=cooker-0.9.2b git checkout ${GITHUB_REF_NAME} mkdir -p ${FLATPAK_DEST}/retrodeck/ From d0d55cb06bdc4b9d771b90697e98628cbb6fd648 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 18 Mar 2025 14:31:25 -0400 Subject: [PATCH 12/52] Removing this export as it just gets lost in the Zenity subshells --- tools/configurator.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index 7892d172..d7352a32 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -109,7 +109,6 @@ rd_zenity --progress --no-cancel --pulsate --auto-close \ configurator_welcome_dialog() { log i "Configurator: opening welcome dialog" - export CONFIGURATOR_GUI="zenity" welcome_menu_options=( "Settings" "Customize your RetroDECK experience with various presets and tweaks." "Open Component" "Manually launch and configure settings for each emulator or component (for advanced users)." From 57ffddec18423b6d423b633ebee626cd85ac8322 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 09:10:47 -0400 Subject: [PATCH 13/52] New userdata backup function, supports full and partial backups from CLI or Configurator --- functions/other_functions.sh | 227 ++++++++++++++++++++++++++++++++++- 1 file changed, 226 insertions(+), 1 deletion(-) diff --git a/functions/other_functions.sh b/functions/other_functions.sh index e1e4cc15..2921fc50 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -354,8 +354,233 @@ update_vita3k_firmware() { } backup_retrodeck_userdata() { + # This function can compress one or more RetroDECK userdata folders into a single zip file for backup. + # The function can do a "standard" backup of all the normal userdata files (which can be very big if there is a lot of media) or a "custom" backup of only specified paths + # The function can take both folder names as defined in retrodeck.cfg or full paths as arguments for folders to backup + # It will also validate that all the provided paths exist and that there is enough free space to perform the backup before actually proceeding. + # It will also rotate backups so that there are only 3 maximum of each type (standard or custom) + # USAGE: backup_retrodeck_userdata standard + # backup_retrodeck_userdata custom saves_folder states_folder /some/other/path + create_dir "$backups_folder" - zip -rq9 "$backups_folder/$(date +"%0m%0d")_retrodeck_userdata.zip" "$saves_folder" "$states_folder" "$bios_folder" "$media_folder" "$themes_folder" "$rdhome/ES-DE/collections" "$rdhome/ES-DE/gamelists" "$logs_folder" "$screenshots_folder" "$mods_folder" "$texture_packs_folder" "$borders_folder" > $logs_folder/$(date +"%0m%0d")_backup_log.log + + backup_date=$(date +"%0m%0d_%H%M") + backup_log_file="$logs_folder/${backup_date}_${backup_type}_backup_log.log" + + # Check if first argument is the type + if [[ "$1" == "standard" || "$1" == "custom" ]]; then + backup_type="$1" + shift # Remove the first argument + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "No valid backup option chosen. Valid options are and ." + fi + log e "No valid backup option chosen. Valid options are and ." + exit 1 + fi + + zip_file="$backups_folder/retrodeck_${backup_date}_${backup_type}.zip" + + # Initialize paths arrays + paths_to_backup=() + declare -A config_paths # Requires an associative (dictionary) array to work + + # Build array of folder names and real paths from retrodeck.cfg + while read -r config_line; do + local current_setting_name=$(get_setting_name "$config_line" "retrodeck") + if [[ ! $current_setting_name =~ (rdhome|sdcard|backups_folder) ]]; then # Ignore these locations + local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "paths") + config_paths["$current_setting_name"]="$current_setting_value" + fi + done < <(grep -v '^\s*$' $rd_conf | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f') + + # Determine which paths to backup + if [[ "$backup_type" == "standard" ]]; then + for folder_name in "${!config_paths[@]}"; do + path_value="${config_paths[$folder_name]}" + if [[ -e "$path_value" ]]; then + paths_to_backup+=("$path_value") + log i "Adding to backup: $folder_name = $path_value" + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The $folder_name was not found at its expected location, $path_value\nSomething may be wrong with your RetroDECK installation." + fi + log i "Warning: Path does not exist: $folder_name = $path_value" + fi + done + + # Add static paths not defined in retrodeck.cfg + if [[ -e "$rdhome/ES-DE/collections" ]]; then + paths_to_backup+=("$rdhome/ES-DE/collections") + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE collections folder was not found at its expected location, $rdhome/ES-DE/collections\nSomething may be wrong with your RetroDECK installation." + fi + log i "Warning: Path does not exist: ES-DE/collections = $rdhome/ES-DE/collections" + fi + + if [[ -e "$rdhome/ES-DE/gamelists" ]]; then + paths_to_backup+=("$rdhome/ES-DE/gamelists") + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE gamelists folder was not found at its expected location, $rdhome/ES-DE/gamelists\nSomething may be wrong with your RetroDECK installation." + fi + log i "Warning: Path does not exist: ES-DE/gamelists = $rdhome/ES-DE/gamelists" + fi + + if [[ -e "$rdhome/ES-DE/custom_systems" ]]; then + paths_to_backup+=("$rdhome/ES-DE/custom_systems") + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE custom_systems folder was not found at its expected location, $rdhome/ES-DE/custom_systems\nSomething may be wrong with your RetroDECK installation." + fi + log i "Warning: Path does not exist: ES-DE/custom_systems = $rdhome/ES-DE/custom_systems" + fi + + # Check if we found any valid paths + if [[ ${#paths_to_backup[@]} -eq 0 ]]; then + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "No valid userdata folders were found.\nSomething may be wrong with your RetroDECK installation." + fi + log e "Error: No valid paths found in config file" + return 1 + fi + + elif [[ "$backup_type" == "custom" ]]; then + if [[ "$#" -eq 0 ]]; then # Check if any paths were provided in the arguments + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "No valid backup locations were specified. Please try again." + fi + log e "Error: No paths specified for custom backup" + return 1 + fi + + # Process each argument - it could be a variable name or a direct path + for arg in "$@"; do + # Check if argument is a variable name in the config + if [[ -n "${config_paths[$arg]}" ]]; then + path_value="${config_paths[$arg]}" + if [[ -e "$path_value" ]]; then + paths_to_backup+=("$path_value") + log i "Added to backup: $arg = $path_value" + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The $arg was not found at its expected location, $path_value.\nSomething may be wrong with your RetroDECK installation." + fi + log e "Error: Path from variable '$arg' does not exist: $path_value" + return 1 + fi + # Otherwise treat it as a direct path + elif [[ -e "$arg" ]]; then + paths_to_backup+=("$arg") + log i "Added to backup: $arg" + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The path $arg was not found at its expected location.\nPlease check the path and try again." + fi + log e "Error: '$arg' is neither a valid variable name nor an existing path" + return 1 + fi + done + fi + + # Calculate total size of selected paths + log i "Calculating size of backup data..." + + total_size=0 + + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then # Show progress dialog if running Zenity Configurator + total_size_file=$(mktemp) # Create temp file for Zenity subshell data extraction + ( + for path in "${paths_to_backup[@]}"; do + if [[ -e "$path" ]]; then + log d "Checking size of path $path" + path_size=$(du -sk "$path" 2>/dev/null | cut -f1) # Get size in KB + path_size=$((path_size * 1024)) # Convert to bytes for calculation + total_size=$((total_size + path_size)) + echo "$total_size" > $total_size_file + fi + done + ) | + rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --title "RetroDECK Configurator Utility - Userdata Backup" \ + --text="Verifying there is enough free space for the backup, please wait..." + total_size=$(cat "$total_size_file") + rm "$total_size_file" # Clean up temp file + else # If running in CLI + for path in "${paths_to_backup[@]}"; do + if [[ -e "$path" ]]; then + log d "Checking size of path $path" + path_size=$(du -sk "$path" 2>/dev/null | cut -f1) # Get size in KB + path_size=$((path_size * 1024)) # Convert to bytes for calculation + total_size=$((total_size + path_size)) + fi + done + fi + + # Get available space at destination + available_space=$(df -B1 "$backups_folder" | awk 'NR==2 {print $4}') + + # Log sizes for reference + log i "Total size of backup data: $(numfmt --to=iec-i --suffix=B $total_size)" + log i "Available space at destination: $(numfmt --to=iec-i --suffix=B $available_space)" + + # Check if we have enough space (using uncompressed size as a conservative estimate) + if [[ "$available_space" -lt "$total_size" ]]; then + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "There is not enough free space to perform this backup.\n\nYou need at least $(numfmt --to=iec-i --suffix=B $total_size),\nplease free up some space and try again." + fi + log e "Error: Not enough space to perform backup. Need at least $(numfmt --to=iec-i --suffix=B $total_size)" + return 1 + fi + + log i "Starting backup process..." + + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then # Show progress dialog if running Zenity Configurator + ( + # Create zip with selected paths + if zip -rq9 "$zip_file" "${paths_to_backup[@]}" >> "$backup_log_file" 2>&1; then + # Rotate backups for the specific type + cd "$backups_folder" || return 1 + ls -t *_${backup_type}.zip | tail -n +4 | xargs -r rm + + final_size=$(du -h "$zip_file" | cut -f1) + configurator_generic_dialog "RetroDECK Userdata Backup" "The backup to $zip_file was successful, final size is $final_size.\n\nThe backups have been rotated, keeping the last 3 of the $backup_type backup type." + log i "Backup completed successfully: $zip_file (Size: $final_size)" + log i "Older backups rotated, keeping latest 3 of type $backup_type" + + if [[ ! -s "$backup_log_file" ]]; then # If the backup log file is empty, meaning zip threw no errors + rm -f "$backup_log_file" + fi + else + configurator_generic_dialog "RetroDECK Userdata Backup" "Something went wrong with the backup process. Please check the log $backup_log_file for more information." + log i "Error: Backup failed" + return 1 + fi + ) | + rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --title "RetroDECK Configurator Utility - Userdata Backup" \ + --text="Compressing files into backup, please wait..." + else + if zip -rq9 "$zip_file" "${paths_to_backup[@]}" >> "$backup_log_file" 2>&1; then + # Rotate backups for the specific type + cd "$backups_folder" || return 1 + ls -t *_${backup_type}.zip | tail -n +4 | xargs -r rm + + final_size=$(du -h "$zip_file" | cut -f1) + log i "Backup completed successfully: $zip_file (Size: $final_size)" + log i "Older backups rotated, keeping latest 3 of type $backup_type" + + if [[ ! -s "$backup_log_file" ]]; then # If the backup log file is empty, meaning zip threw no errors + rm -f "$backup_log_file" + fi + else + log i "Error: Backup failed" + return 1 + fi + fi } make_name_pretty() { From e3585075200798b5f96bbf53dfa1e216724184a1 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 09:11:11 -0400 Subject: [PATCH 14/52] Update userdata backup process in Zenity Configurator --- tools/configurator.sh | 56 +++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index d7352a32..42a48626 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -461,21 +461,53 @@ configurator_tools_dialog() { case $choice in "Backup Userdata" ) + log i "Configurator: opening \"$choice\" menu" - configurator_generic_dialog "RetroDECK Configurator - Backup Userdata" "This tool will compress important RetroDECK userdata (basically everything except the ROMs folder) into a zip file.\n\nThis process can take several minutes, and the resulting zip file can be found in the ~/retrodeck/backups folder." - ( - backup_retrodeck_userdata - ) | - rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ - --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ - --title "RetroDECK Configurator Utility - Backup in Progress" \ - --text="Backing up RetroDECK userdata, please wait..." - if [[ -f "$backups_folder/$(date +"%0m%0d")_retrodeck_userdata.zip" ]]; then - configurator_generic_dialog "RetroDECK Configurator - Backup Userdata" "The backup process is now complete." + configurator_generic_dialog "RetroDECK Configurator - Backup Userdata" "This tool will compress one or more RetroDECK userdata folders into a single zip file.\n\nThis process can take several minutes, and the resulting zip file can be found in the ~/retrodeck/backups folder." + + choice=$(rd_zenity --title "RetroDECK Configurator Utility - Backup Userdata" --info --no-wrap --ok-label="Cancel" --extra-button="Backup Some Userdata" --extra-button="Backup All Userdata" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --text="Would you like to compress all RetroDECK userdata folders, or only some of them?") + + local rc=$? + if [[ $rc == "0" ]] && [[ -z "$choice" ]]; then # User selected Cancel button + configurator_tools_dialog else - configurator_generic_dialog "RetroDECK Configurator - Backup Userdata" "The backup process could not be completed,\nplease check the logs folder for more information." + case $choice in + "Backup Some Userdata" ) + while read -r config_line; do + local current_setting_name=$(get_setting_name "$config_line" "retrodeck") + if [[ ! $current_setting_name =~ (rdhome|sdcard|backups_folder) ]]; then # Ignore these locations + log d "Adding $current_setting_name to compressible paths." + local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "paths") + compressible_paths=("${compressible_paths[@]}" "false" "$current_setting_name" "$current_setting_value") + fi + done < <(grep -v '^\s*$' $rd_conf | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f') + + choice=$(rd_zenity \ + --list --width=1200 --height=720 \ + --checklist \ + --separator="^" \ + --print-column=3 \ + --text="Please select folders to compress..." \ + --column "Backup?" \ + --column "Folder Name" \ + --column "Path" \ + "${compressible_paths[@]}") + + choices=() # Expand choice string into passable array + IFS='^' read -ra choices <<< "$choice" + + export CONFIGURATOR_GUI="zenity" + backup_retrodeck_userdata "custom" "${choices[@]}" # Expand array of choices into individual arguments + ;; + "Backup All Userdata" ) + export CONFIGURATOR_GUI="zenity" + backup_retrodeck_userdata "standard" + ;; + esac + + configurator_tools_dialog fi - configurator_tools_dialog ;; "BIOS Checker" ) From ebe8aeee6b7765518ce427c32961d7ea76c1a80d Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 09:36:28 -0400 Subject: [PATCH 15/52] Add optional userdata backup to post_update process --- functions/post_update.sh | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/functions/post_update.sh b/functions/post_update.sh index 7d6f8a46..0d1f3c84 100644 --- a/functions/post_update.sh +++ b/functions/post_update.sh @@ -9,6 +9,54 @@ post_update() { update_rd_conf + # Optional userdata backup prior to update + + choice=$(rd_zenity --title "RetroDECK Update - Backup Userdata" --info --no-wrap --ok-label="No Backup" --extra-button="Backup Some Userdata" --extra-button="Backup All Userdata" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --text="Would you like to backup some or all of the RetroDECK userdata prior to update?\n\nIf you choose \"Backup Some Userdata\" you will be given a choice of which folders to backup.\nPLEASE NOTE: A full backup may take up a large amount of space, especially if you have a lot of scraped media.") + + local rc=$? + if [[ $rc == "0" ]] && [[ -z "$choice" ]]; then # User selected No Backup button + log i "User chose to not backup prior to update." + else + case $choice in + "Backup Some Userdata" ) + log i "User chose to backup some userdata prior to update." + while read -r config_line; do + local current_setting_name=$(get_setting_name "$config_line" "retrodeck") + if [[ ! $current_setting_name =~ (rdhome|sdcard|backups_folder) ]]; then # Ignore these locations + log d "Adding $current_setting_name to compressible paths." + local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "paths") + compressible_paths=("${compressible_paths[@]}" "false" "$current_setting_name" "$current_setting_value") + fi + done < <(grep -v '^\s*$' $rd_conf | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f') + + choice=$(rd_zenity \ + --list --width=1200 --height=720 \ + --checklist \ + --separator="^" \ + --print-column=3 \ + --text="Please select folders to compress..." \ + --column "Backup?" \ + --column "Folder Name" \ + --column "Path" \ + "${compressible_paths[@]}") + + choices=() # Expand choice string into passable array + IFS='^' read -ra choices <<< "$choice" + + export CONFIGURATOR_GUI="zenity" + backup_retrodeck_userdata "custom" "${choices[@]}" # Expand array of choices into individual arguments + ;; + "Backup All Userdata" ) + log i "User chose to backup all userdata prior to update." + export CONFIGURATOR_GUI="zenity" + backup_retrodeck_userdata "standard" + ;; + esac + fi + + # Start of post_update actions + if [[ $(check_version_is_older_than "$version_being_updated" "0.5.0b") == "true" ]]; then # If updating from prior to save sorting change at 0.5.0b log d "Version is older than 0.5.0b, executing save migration" save_migration From da4603cdd1e4d855f145d5005d8987fe8f559194 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 10:52:57 -0400 Subject: [PATCH 16/52] Rework USB Import tool order of operations --- tools/configurator.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index 42a48626..bafa5c1f 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -1422,7 +1422,6 @@ configurator_usb_import_dialog() { "Prepare USB device" ) log i "Configurator: opening \"$choice\" menu" - configurator_generic_dialog "RetroDeck Configurator - USB Import" "If you have an SD card installed that is not currently configured in RetroDECK it may show up in this list, but not be suitable for USB import.\n\nPlease select your desired drive carefully." external_devices=() @@ -1432,6 +1431,7 @@ configurator_usb_import_dialog() { done < <(df --output=size,target -h | grep "/run/media/" | grep -v "$sdcard" | awk '{$1=$1;print}') if [[ "${#external_devices[@]}" -gt 0 ]]; then + configurator_generic_dialog "RetroDeck Configurator - USB Import" "If you have an SD card installed that is not currently configured in RetroDECK it may show up in this list, but not be suitable for USB import.\n\nPlease select your desired drive carefully." choice=$(rd_zenity --list --title="RetroDECK Configurator Utility - USB Migration Tool" --cancel-label="Back" \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --width=1200 --height=720 \ --hide-column=3 --print-column=3 \ @@ -1441,9 +1441,10 @@ configurator_usb_import_dialog() { "${external_devices[@]}") if [[ ! -z "$choice" ]]; then + create_dir "$choice/RetroDECK Import" es-de --home "$choice/RetroDECK Import" --create-system-dirs rm -rf "$choice/RetroDECK Import/ES-DE" # Cleanup unnecessary folder - create_dir "$choice/RetroDECK Import/BIOS" + # Prepare default BIOS folder subfolders create_dir "$choice/RetroDECK Import/BIOS/np2kai" From 8d2b0220326fa5829b0d615c9d8114b336cbbb40 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 11:01:19 -0400 Subject: [PATCH 17/52] Clarify wording on empty rom folder cleanup menu --- tools/configurator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index bafa5c1f..4e9edea2 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -700,7 +700,7 @@ configurator_data_management_dialog() { --list --width=1200 --height=720 --title "RetroDECK Configurator - RetroDECK: Clean Empty ROM Folders" \ --checklist --hide-column=3 --ok-label="Remove Selected" --extra-button="Remove All" \ --separator="," --print-column=2 \ - --text="Choose which ROM folders to remove:" \ + --text="Choose which empty ROM folders to remove:" \ --column "Remove?" \ --column "System" \ "${empty_rom_folders_list[@]}") From 328b3982cf037fb08315ec369fc99209a10c2e4d Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 11:12:33 -0400 Subject: [PATCH 18/52] Fix empty rom folder cleanup handling of paths with spaces --- functions/configurator_functions.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/functions/configurator_functions.sh b/functions/configurator_functions.sh index 2d68829c..78dd78bb 100644 --- a/functions/configurator_functions.sh +++ b/functions/configurator_functions.sh @@ -23,14 +23,14 @@ find_empty_rom_folders() { if [[ $count -eq 0 ]]; then # Directory is empty - empty_rom_folders_list=("${empty_rom_folders_list[@]}" "false" "$(realpath $dir)") - all_empty_folders=("${all_empty_folders[@]}" "$(realpath $dir)") - echo "$(realpath $dir)" >> "$godot_empty_roms_folders" # Godot data transfer temp file + empty_rom_folders_list=("${empty_rom_folders_list[@]}" "false" "$(realpath "$dir")") + all_empty_folders=("${all_empty_folders[@]}" "$(realpath "$dir")") + echo "$(realpath "$dir")" >> "$godot_empty_roms_folders" # Godot data transfer temp file elif [[ $count -eq 1 ]] && [[ "$(basename "${files[0]}")" == "systeminfo.txt" ]]; then # Directory contains only systeminfo.txt - empty_rom_folders_list=("${empty_rom_folders_list[@]}" "false" "$(realpath $dir)") - all_empty_folders=("${all_empty_folders[@]}" "$(realpath $dir)") - echo "$(realpath $dir)" >> "$godot_empty_roms_folders" # Godot data transfer temp file + empty_rom_folders_list=("${empty_rom_folders_list[@]}" "false" "$(realpath "$dir")") + all_empty_folders=("${all_empty_folders[@]}" "$(realpath "$dir")") + echo "$(realpath "$dir")" >> "$godot_empty_roms_folders" # Godot data transfer temp file elif [[ $count -eq 2 ]] && [[ "$files" =~ "systeminfo.txt" ]]; then contains_helper_file="false" for helper_file in "${all_helper_files[@]}" # Compare helper file list to dir file list @@ -42,9 +42,9 @@ find_empty_rom_folders() { done if [[ "$contains_helper_file" == "true" ]]; then # Directory contains only systeminfo.txt and a helper file - empty_rom_folders_list=("${empty_rom_folders_list[@]}" "false" "$(realpath $dir)") - all_empty_folders=("${all_empty_folders[@]}" "$(realpath $dir)") - echo "$(realpath $dir)" >> "$godot_empty_roms_folders" # Godot data transfer temp file + empty_rom_folders_list=("${empty_rom_folders_list[@]}" "false" "$(realpath "$dir")") + all_empty_folders=("${all_empty_folders[@]}" "$(realpath "$dir")") + echo "$(realpath "$dir")" >> "$godot_empty_roms_folders" # Godot data transfer temp file fi fi done From 1620a5441ea6bbac16a94a670208ef76d68b5f91 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 11:25:54 -0400 Subject: [PATCH 19/52] Stop Pico-8 folder from being recreated after a roms folder move if it has already been removed (such as by empty folder cleanup) --- functions/prepare_component.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/functions/prepare_component.sh b/functions/prepare_component.sh index 52b043b2..26debdbe 100644 --- a/functions/prepare_component.sh +++ b/functions/prepare_component.sh @@ -561,8 +561,10 @@ prepare_component() { if [[ "$component" =~ ^(pico8|pico-8|all)$ ]]; then component_found="true" if [[ ("$action" == "reset") || ("$action" == "postmove") ]]; then + if [[ -d "$roms_folder/pico8" ]]; then + dir_prep "$roms_folder/pico8" "$bios_folder/pico-8/carts" # Symlink default game location to RD roms for cleanliness (this location is overridden anyway by the --root_path launch argument anyway) + fi dir_prep "$bios_folder/pico-8" "$HOME/.lexaloffle/pico-8" # Store binary and config files together. The .lexaloffle directory is a hard-coded location for the PICO-8 config file, cannot be changed - dir_prep "$roms_folder/pico8" "$bios_folder/pico-8/carts" # Symlink default game location to RD roms for cleanliness (this location is overridden anyway by the --root_path launch argument anyway) dir_prep "$saves_folder/pico-8" "$bios_folder/pico-8/cdata" # PICO-8 saves folder cp -fv "$config/pico-8/config.txt" "$bios_folder/pico-8/config.txt" cp -fv "$config/pico-8/sdl_controllers.txt" "$bios_folder/pico-8/sdl_controllers.txt" From a7f9c5b94c91c6fb4b24e24d3cc359bd39a077f2 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 11:28:36 -0400 Subject: [PATCH 20/52] Fix Configurator move folder menu flow --- functions/dialogs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/dialogs.sh b/functions/dialogs.sh index 23ca4433..065cde1b 100644 --- a/functions/dialogs.sh +++ b/functions/dialogs.sh @@ -128,7 +128,7 @@ configurator_move_folder_dialog() { if [[ -z $(ls -1 "$source_root/retrodeck") ]]; then # Cleanup empty old_path/retrodeck folder if it was left behind rmdir "$source_root/retrodeck" fi - configurator_process_complete_dialog "moving the RetroDECK data directory to internal storage" + configurator_generic_dialog "RetroDECK Configurator - Move Folder" "moving the RetroDECK data directory to internal storage" else configurator_generic_dialog "RetroDECK Configurator - Move Folder" "The moving process was not completed, please try again." fi From aa14947eba88f1e0ba6d9a4f9650a70834d8c6b8 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 11:41:36 -0400 Subject: [PATCH 21/52] Fix instances where realpath and basename would not properly handle arguments that contained spaces --- functions/dialogs.sh | 18 +++++++++--------- functions/other_functions.sh | 4 ++-- functions/steam_sync.sh | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/functions/dialogs.sh b/functions/dialogs.sh index 065cde1b..6e1a5670 100644 --- a/functions/dialogs.sh +++ b/functions/dialogs.sh @@ -92,11 +92,11 @@ configurator_move_folder_dialog() { if [[ ! "$rd_dir_name" == "rdhome" ]]; then # If a sub-folder is being moved, find it's path without the source_root. So /home/deck/retrodeck/roms becomes retrodeck/roms local rd_dir_path="$(echo "$dir_to_move" | sed "s/.*\(retrodeck\/.*\)/\1/; s/\/$//")" else # Otherwise just set the retrodeck root folder - local rd_dir_path="$(basename $dir_to_move)" + local rd_dir_path="$(basename "$dir_to_move")" fi if [[ -d "$dir_to_move" ]]; then # If the directory selected to move already exists at the expected location pulled from retrodeck.cfg - choice=$(configurator_destination_choice_dialog "RetroDECK Data" "Please choose a destination for the $(basename $dir_to_move) folder.") + choice=$(configurator_destination_choice_dialog "RetroDECK Data" "Please choose a destination for the $(basename "$dir_to_move") folder.") case $choice in "Internal Storage" | "SD Card" | "Custom Location" ) # If the user picks a location @@ -105,17 +105,17 @@ configurator_move_folder_dialog() { elif [[ "$choice" == "SD Card" ]]; then # If the user wants to move the folder to the predefined SD card location, set the target as sdcard from retrodeck.cfg local dest_root="$sdcard" else - configurator_generic_dialog "RetroDECK Configurator - Move Folder" "Select the parent folder you would like to store the $(basename $dir_to_move) folder in." + configurator_generic_dialog "RetroDECK Configurator - Move Folder" "Select the parent folder you would like to store the $(basename "$dir_to_move") folder in." local dest_root=$(directory_browse "RetroDECK directory location") # Set the destination root as the selected custom location fi if [[ (! -z "$dest_root") && ( -w "$dest_root") ]]; then # If user picked a destination and it is writable - if [[ (-d "$dest_root/$rd_dir_path") && (! -L "$dest_root/$rd_dir_path") && (! $rd_dir_name == "rdhome") ]] || [[ "$(realpath $dir_to_move)" == "$dest_root/$rd_dir_path" ]]; then # If the user is trying to move the folder to where it already is (excluding symlinks that will be unlinked) - configurator_generic_dialog "RetroDECK Configurator - Move Folder" "The $(basename $dir_to_move) folder is already at that location, please pick a new one." + if [[ (-d "$dest_root/$rd_dir_path") && (! -L "$dest_root/$rd_dir_path") && (! $rd_dir_name == "rdhome") ]] || [[ "$(realpath "$dir_to_move")" == "$dest_root/$rd_dir_path" ]]; then # If the user is trying to move the folder to where it already is (excluding symlinks that will be unlinked) + configurator_generic_dialog "RetroDECK Configurator - Move Folder" "The $(basename "$dir_to_move") folder is already at that location, please pick a new one." configurator_move_folder_dialog "$rd_dir_name" else if [[ $(verify_space "$(echo $dir_to_move | sed 's/\/$//')" "$dest_root") ]]; then # Make sure there is enough space at the destination - configurator_generic_dialog "RetroDECK Configurator - Move Folder" "Moving $(basename $dir_to_move) folder to $choice" + configurator_generic_dialog "RetroDECK Configurator - Move Folder" "Moving $(basename "$dir_to_move") folder to $choice" unlink "$dest_root/$rd_dir_path" # In case there is already a symlink at the picked destination move "$dir_to_move" "$dest_root/$rd_dir_path" if [[ -d "$dest_root/$rd_dir_path" ]]; then # If the move succeeded @@ -150,12 +150,12 @@ configurator_move_folder_dialog() { esac else # The folder to move was not found at the path pulled from retrodeck.cfg and it needs to be reconfigured manually. - configurator_generic_dialog "RetroDECK Configurator - Move Folder" "The $(basename $dir_to_move) folder was not found at the expected location.\n\nThis may have happened if the folder was moved manually.\n\nPlease select the current location of the folder." - dir_to_move=$(directory_browse "RetroDECK $(basename $dir_to_move) directory location") + configurator_generic_dialog "RetroDECK Configurator - Move Folder" "The $(basename "$dir_to_move") folder was not found at the expected location.\n\nThis may have happened if the folder was moved manually.\n\nPlease select the current location of the folder." + dir_to_move=$(directory_browse "RetroDECK $(basename "$dir_to_move") directory location") declare -g "$rd_dir_name=$dir_to_move" prepare_component "postmove" "all" conf_write - configurator_generic_dialog "RetroDECK Configurator - Move Folder" "RetroDECK $(basename $dir_to_move) folder now configured at\n$dir_to_move." + configurator_generic_dialog "RetroDECK Configurator - Move Folder" "RetroDECK $(basename "$dir_to_move") folder now configured at\n$dir_to_move." configurator_move_folder_dialog "$rd_dir_name" fi } diff --git a/functions/other_functions.sh b/functions/other_functions.sh index 2921fc50..61177835 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -273,8 +273,8 @@ dir_prep() { # Call me with: # dir prep "real dir" "symlink location" - real="$(realpath -s $1)" - symlink="$(realpath -s $2)" + real="$(realpath -s "$1")" + symlink="$(realpath -s "$2")" log d "Preparing directory $symlink in $real" diff --git a/functions/steam_sync.sh b/functions/steam_sync.sh index c12c289f..64a6077f 100644 --- a/functions/steam_sync.sh +++ b/functions/steam_sync.sh @@ -71,7 +71,7 @@ add_to_steam() { echo "$command" >> "$launcher_tmp" chmod +x "$launcher_tmp" else - log d "$(basename $launcher) desktop file already exists" + log d "$(basename "$launcher") desktop file already exists" fi fi From 2c296b6fdb2d69841a6dfe2da68fd4b74a6b4ff5 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 14:04:28 -0400 Subject: [PATCH 22/52] Fix Zenity Configurator multi-file compression menu flow --- tools/configurator.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/configurator.sh b/tools/configurator.sh index 4e9edea2..a14ad183 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -757,26 +757,31 @@ configurator_compression_tool_dialog() { "Compress Multiple Games: CHD" ) log i "Configurator: opening \"$choice\" menu" configurator_compress_multiple_games_dialog "chd" + configurator_compression_tool_dialog ;; "Compress Multiple Games: ZIP" ) log i "Configurator: opening \"$choice\" menu" configurator_compress_multiple_games_dialog "zip" + configurator_compression_tool_dialog ;; "Compress Multiple Games: RVZ" ) log i "Configurator: opening \"$choice\" menu" configurator_compress_multiple_games_dialog "rvz" + configurator_compression_tool_dialog ;; "Compress Multiple Games: All Formats" ) log i "Configurator: opening \"$choice\" menu" configurator_compress_multiple_games_dialog "all" + configurator_compression_tool_dialog ;; "Compress All Games" ) log i "Configurator: opening \"$choice\" menu" configurator_compress_multiple_games_dialog "everything" + configurator_compression_tool_dialog ;; "" ) # No selection made or Back button clicked From d423ed7fe79c131fa726448b99aff7d30ef85a25 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Wed, 19 Mar 2025 14:21:09 -0400 Subject: [PATCH 23/52] During compression checks, only look for files in folders that exist --- functions/compression.sh | 98 +++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/functions/compression.sh b/functions/compression.sh index cf24c490..e70fb948 100644 --- a/functions/compression.sh +++ b/functions/compression.sh @@ -163,57 +163,61 @@ find_compatible_games() { while IFS= read -r system; do log d "Checking system: $system" - local compression_candidates - compression_candidates=$(find "$roms_folder/$system" -type f -not -iname "*.txt") - if [[ -n "$compression_candidates" ]]; then - while IFS= read -r game; do - log d "Checking game: $game" - local compatible_compression_format - compatible_compression_format=$(find_compatible_compression_format "$game") - local file_ext="${game##*.}" - case "$compression_format" in - "chd") - if [[ "$compatible_compression_format" == "chd" ]]; then - if [[ "$file_ext" == "chd" ]]; then - log d "Skipping $game because it is already a CHD file." - elif [[ ! -f "${game%.*}.chd" ]]; then - log d "Game $game is compatible with CHD compression" - echo "${game}^chd" >> "$output_file" + if [[ -d "$roms_folder/$system" ]]; then + local compression_candidates + compression_candidates=$(find "$roms_folder/$system" -type f -not -iname "*.txt") + if [[ -n "$compression_candidates" ]]; then + while IFS= read -r game; do + log d "Checking game: $game" + local compatible_compression_format + compatible_compression_format=$(find_compatible_compression_format "$game") + local file_ext="${game##*.}" + case "$compression_format" in + "chd") + if [[ "$compatible_compression_format" == "chd" ]]; then + if [[ "$file_ext" == "chd" ]]; then + log d "Skipping $game because it is already a CHD file." + elif [[ ! -f "${game%.*}.chd" ]]; then + log d "Game $game is compatible with CHD compression" + echo "${game}^chd" >> "$output_file" + fi fi - fi - ;; - "zip") - if [[ "$compatible_compression_format" == "zip" ]]; then - if [[ "$file_ext" == "zip" ]]; then - log d "Skipping $game because it is already a ZIP file." - elif [[ ! -f "${game%.*}.zip" ]]; then - log d "Game $game is compatible with ZIP compression" - echo "${game}^zip" >> "$output_file" + ;; + "zip") + if [[ "$compatible_compression_format" == "zip" ]]; then + if [[ "$file_ext" == "zip" ]]; then + log d "Skipping $game because it is already a ZIP file." + elif [[ ! -f "${game%.*}.zip" ]]; then + log d "Game $game is compatible with ZIP compression" + echo "${game}^zip" >> "$output_file" + fi fi - fi - ;; - "rvz") - if [[ "$compatible_compression_format" == "rvz" ]]; then - if [[ "$file_ext" == "rvz" ]]; then - log d "Skipping $game because it is already an RVZ file." - elif [[ ! -f "${game%.*}.rvz" ]]; then - log d "Game $game is compatible with RVZ compression" - echo "${game}^rvz" >> "$output_file" + ;; + "rvz") + if [[ "$compatible_compression_format" == "rvz" ]]; then + if [[ "$file_ext" == "rvz" ]]; then + log d "Skipping $game because it is already an RVZ file." + elif [[ ! -f "${game%.*}.rvz" ]]; then + log d "Game $game is compatible with RVZ compression" + echo "${game}^rvz" >> "$output_file" + fi fi - fi - ;; - "all") - if [[ "$compatible_compression_format" != "none" ]]; then - if [[ "$file_ext" == "$compatible_compression_format" ]]; then - log d "Skipping $game because it is already in $compatible_compression_format format." - else - log d "Game $game is compatible with $compatible_compression_format compression" - echo "${game}^${compatible_compression_format}" >> "$output_file" + ;; + "all") + if [[ "$compatible_compression_format" != "none" ]]; then + if [[ "$file_ext" == "$compatible_compression_format" ]]; then + log d "Skipping $game because it is already in $compatible_compression_format format." + else + log d "Game $game is compatible with $compatible_compression_format compression" + echo "${game}^${compatible_compression_format}" >> "$output_file" + fi fi - fi - ;; - esac - done < <(printf '%s\n' "$compression_candidates") + ;; + esac + done < <(printf '%s\n' "$compression_candidates") + fi + else + log i "Rom folder for $system is missing, skipping" fi done < <(printf '%s\n' "$compressable_systems_list") From d6708c1c2b6afe3a3d603f6ad2b12df95acbf7a8 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 20 Mar 2025 15:31:24 -0400 Subject: [PATCH 24/52] Repair Configurator multi-file compression logic --- tools/configurator.sh | 114 +++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 74 deletions(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index a14ad183..fadbc8ef 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -822,120 +822,86 @@ configurator_compress_single_game_dialog() { configurator_compress_multiple_games_dialog() { log d "Starting to compress \"$1\"" - local output_file="${godot_compression_compatible_games}" - [ -f "$output_file" ] && rm -f "$output_file" - touch "$output_file" + local compressible_games_list_file="${godot_compression_compatible_games}" + [ -f "$compressible_games_list_file" ] && rm -f "$compressible_games_list_file" + touch "$compressible_games_list_file" - ## --- SEARCH PHASE WITH LOADING SCREEN --- - local progress_pipe - progress_pipe=$(mktemp -u) - mkfifo "$progress_pipe" - - # Launch find_compatible_games in the background (its output goes to the file) - find_compatible_games "$1" > "$output_file" & - local finder_pid=$! - - # Launch a background process that writes loading messages until the search completes. ( - while kill -0 "$finder_pid" 2>/dev/null; do - echo "# Loading: Searching for compatible games..." - sleep 1 - done - echo "100" - ) > "$progress_pipe" & - local progress_writer_pid=$! + find_compatible_games "$1" > "$compressible_games_list_file" + ) | + rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --auto-close \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --title "RetroDECK Configurator - RetroDECK: Compression Tool" --text "RetroDECK is searching for compress1ble games, please wait..." - rd_zenity --progress --pulsate --auto-close \ - --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ - --title="RetroDECK Configurator Utility - Searching for Compressable Games" \ - --text="Searching for compressable games, please wait..." < "$progress_pipe" - - wait "$finder_pid" - wait "$progress_writer_pid" - rm "$progress_pipe" - - if [[ -s "$output_file" ]]; then - mapfile -t all_compressable_games < "$output_file" - log d "Found the following games to compress: ${all_compressable_games[*]}" + if [[ -s "$compressible_games_list_file" ]]; then + mapfile -t all_compressible_games < "$compressible_games_list_file" + log d "Found the following games to compress: ${all_compressible_games[*]}" else - configurator_generic_dialog "RetroDECK Configurator - Compression Tool" "No compressable files were found." - return + configurator_generic_dialog "RetroDECK Configurator - Compression Tool" "No compressible files were found." + configurator_compression_tool_dialog fi local games_to_compress=() if [[ "$1" != "everything" ]]; then local checklist_entries=() - for line in "${all_compressable_games[@]}"; do + for line in "${all_compressible_games[@]}"; do IFS="^" read -r game comp <<< "$line" local short_game="${game#$roms_folder}" checklist_entries+=( "TRUE" "$short_game" "$line" ) done - local choice - choice=$(rd_zenity \ + local choice=$(rd_zenity \ --list --width=1200 --height=720 --title "RetroDECK Configurator - Compression Tool" \ --checklist --hide-column=3 --ok-label="Compress Selected" --extra-button="Compress All" \ - --separator="," --print-column=3 \ + --separator=$'\0' --print-column=3 \ --text="Choose which games to compress:" \ --column "Compress?" \ --column "Game" \ - --column "Game Full Path" \ + --column "Game Full Path and Compression Format" \ "${checklist_entries[@]}") local rc=$? log d "User choice: $choice" if [[ $rc == 0 && -n "$choice" ]]; then - IFS="," read -ra games_to_compress <<< "$choice" + while IFS="^" read -r game comp; do # Split Zenity choice string into compatible pairs (game^format) + games_to_compress+=("$game"^"$comp") + done < "$compressible_games_list_file" elif [[ -n "$choice" ]]; then - games_to_compress=("${all_compressable_games[@]}") + games_to_compress=("${all_compressible_games[@]}") else - return + configurator_compression_tool_dialog fi else - games_to_compress=("${all_compressable_games[@]}") + games_to_compress=("${all_compressible_games[@]}") fi + local post_compression_cleanup=$(configurator_compression_cleanup_dialog) + local total_games=${#games_to_compress[@]} local games_left=$total_games - ## --- COMPRESSION PHASE WITH PROGRESS SCREEN --- - local comp_pipe - comp_pipe=$(mktemp -u) - mkfifo "$comp_pipe" - ( - for game_line in "${games_to_compress[@]}"; do - IFS="^" read -r game compression_format <<< "$game_line" - local system - system=$(echo "$game" | grep -oE "$roms_folder/[^/]+" | grep -oE "[^/]+$") - log i "Compressing $(basename "$game") into $compression_format format" + for game_line in "${games_to_compress[@]}"; do + IFS="^" read -r game compression_format <<< "$game_line" - # Launch the compression in the background. - compress_game "$compression_format" "$game" "$system" & - local comp_pid=$! + local system + system=$(echo "$game" | grep -oE "$roms_folder/[^/]+" | grep -oE "[^/]+$") + log i "Compressing $(basename "$game") into $compression_format format" - # While the compression is in progress, write a status message every second. - while kill -0 "$comp_pid" 2>/dev/null; do - echo "# Compressing $(basename "$game") into $compression_format format" - sleep 1 - done - - # When finished, update the progress percentage. - local progress=$(( 100 - (( 100 / total_games ) * games_left) )) - echo "$progress" - games_left=$(( games_left - 1 )) - done - echo "100" - ) > "$comp_pipe" & - local comp_pid_group=$! + echo "#Compressing $(basename "$game") into $compression_format format.\n\n$games_left games left to compress." # Update Zenity dialog text + compress_game "$compression_format" "$game" "$post_compression_cleanup" "$system" + games_left=$(( games_left - 1 )) + local progress=$(( 99 - (( 99 / total_games ) * games_left) )) + log d "progress: $progress" + echo "$progress" # Update Zenity dialog progress bar + done + echo "100" # Close Zenity progress dialog when finished + ) | rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --auto-close \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck/retrodeck.svg" \ --width="800" \ - --title "RetroDECK Configurator Utility - Compression in Progress" < "$comp_pipe" - - wait "$comp_pid_group" - rm "$comp_pipe" + --title "RetroDECK Configurator Utility - Compression in Progress" configurator_generic_dialog "RetroDECK Configurator - Compression Tool" "The compression process is complete!" configurator_compression_tool_dialog From b637ddd72182823e059c610bce68879ea4f122b4 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 20 Mar 2025 15:33:01 -0400 Subject: [PATCH 25/52] Update compress_game to accept post-compression-cleanup argument directly --- functions/compression.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/functions/compression.sh b/functions/compression.sh index e70fb948..247eb442 100644 --- a/functions/compression.sh +++ b/functions/compression.sh @@ -2,7 +2,7 @@ compress_game() { # Function for compressing one or more files to .chd format - # USAGE: compress_game $format $full_path_to_input_file $system(optional) + # USAGE: compress_game $format $full_path_to_input_file $cleanup_choice $system(optional) local file="$2" local filename_no_path=$(basename "$file") local filename_no_extension="${filename_no_path%.*}" @@ -11,8 +11,9 @@ compress_game() { local dest_file=$(dirname "$(realpath "$file")")"/""$filename_no_extension" if [[ "$1" == "chd" ]]; then - case "$3" in # Check platform-specific compression options + case "$4" in # Check platform-specific compression options "psp" ) + log d "Compressing PSP game $source_file into $dest_file" /app/bin/chdman createdvd --hunksize 2048 -i "$source_file" -o "$dest_file".chd -c zstd ;; "ps2" ) @@ -32,7 +33,7 @@ compress_game() { dolphin-tool convert -f rvz -b 131072 -c zstd -l 5 -i "$source_file" -o "$dest_file.rvz" fi - if [[ $post_compression_cleanup == "true" ]]; then # Remove file(s) if requested + if [[ "$3" == "true" ]]; then # Remove file(s) if requested if [[ -f "${file%.*}.$1" ]]; then log i "Performing post-compression file cleanup" if [[ "$file" == *".cue" ]]; then From cf750aa58d939d52e352927c73f67f9955da4301 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 20 Mar 2025 15:33:45 -0400 Subject: [PATCH 26/52] Improve find_compatible_games logic and spelling --- functions/compression.sh | 56 ++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/functions/compression.sh b/functions/compression.sh index 247eb442..956aa4b3 100644 --- a/functions/compression.sh +++ b/functions/compression.sh @@ -150,13 +150,13 @@ find_compatible_games() { compression_format="$1" fi - local compressable_systems_list + local compressible_systems_list if [[ "$compression_format" == "all" ]]; then - compressable_systems_list=$(jq -r '.compression_targets | to_entries[] | .value[]' "$features") - log d "compressable_systems_list: $compressable_systems_list" + compressible_systems_list=$(jq -r '.compression_targets | to_entries[] | .value[]' "$features") + log d "compressible_systems_list: $compressible_systems_list" else - compressable_systems_list=$(jq -r '.compression_targets["'"$compression_format"'"][]' "$features") - log d "compressable_systems_list: $compressable_systems_list" + compressible_systems_list=$(jq -r '.compression_targets["'"$compression_format"'"][]' "$features") + log d "compressible_systems_list: $compressible_systems_list" fi log d "Finding compatible games for compression ($1)" @@ -172,55 +172,43 @@ find_compatible_games() { log d "Checking game: $game" local compatible_compression_format compatible_compression_format=$(find_compatible_compression_format "$game") + if [[ -f "${game%.*}.$compatible_compression_format" ]]; then # If a compressed version of this game already exists + log d "Skipping $game because a $compatible_compression_format version already exists." + continue + fi local file_ext="${game##*.}" case "$compression_format" in "chd") if [[ "$compatible_compression_format" == "chd" ]]; then - if [[ "$file_ext" == "chd" ]]; then - log d "Skipping $game because it is already a CHD file." - elif [[ ! -f "${game%.*}.chd" ]]; then - log d "Game $game is compatible with CHD compression" - echo "${game}^chd" >> "$output_file" - fi + log d "Game $game is compatible with CHD compression" + echo "${game}^chd" >> "$output_file" fi ;; "zip") if [[ "$compatible_compression_format" == "zip" ]]; then - if [[ "$file_ext" == "zip" ]]; then - log d "Skipping $game because it is already a ZIP file." - elif [[ ! -f "${game%.*}.zip" ]]; then - log d "Game $game is compatible with ZIP compression" - echo "${game}^zip" >> "$output_file" - fi + log d "Game $game is compatible with ZIP compression" + echo "${game}^zip" >> "$output_file" fi ;; "rvz") if [[ "$compatible_compression_format" == "rvz" ]]; then - if [[ "$file_ext" == "rvz" ]]; then - log d "Skipping $game because it is already an RVZ file." - elif [[ ! -f "${game%.*}.rvz" ]]; then - log d "Game $game is compatible with RVZ compression" - echo "${game}^rvz" >> "$output_file" - fi + log d "Game $game is compatible with RVZ compression" + echo "${game}^rvz" >> "$output_file" fi ;; "all") if [[ "$compatible_compression_format" != "none" ]]; then - if [[ "$file_ext" == "$compatible_compression_format" ]]; then - log d "Skipping $game because it is already in $compatible_compression_format format." - else - log d "Game $game is compatible with $compatible_compression_format compression" - echo "${game}^${compatible_compression_format}" >> "$output_file" - fi + log d "Game $game is compatible with $compatible_compression_format compression" + echo "${game}^${compatible_compression_format}" >> "$output_file" fi ;; esac done < <(printf '%s\n' "$compression_candidates") fi else - log i "Rom folder for $system is missing, skipping" + log d "Rom folder for $system is missing, skipping" fi - done < <(printf '%s\n' "$compressable_systems_list") + done < <(printf '%s\n' "$compressible_systems_list") log d "Compatible games have been written to $output_file" cat "$output_file" @@ -262,9 +250,9 @@ cli_compress_all_games() { local compressable_game="" local all_compressable_games=() if [[ $compression_format == "all" ]]; then - local compressable_systems_list=$(jq -r '.compression_targets | to_entries[] | .value[]' $features) + local compressible_systems_list=$(jq -r '.compression_targets | to_entries[] | .value[]' $features) else - local compressable_systems_list=$(jq -r '.compression_targets["'"$compression_format"'"][]' $features) + local compressible_systems_list=$(jq -r '.compression_targets["'"$compression_format"'"][]' $features) fi read -p "Do you want to have the original files removed after compression is complete? Please answer y/n and press Enter: " post_compression_cleanup @@ -288,5 +276,5 @@ cli_compress_all_games() { else log w "No compatible files found for compression in $system" fi - done < <(printf '%s\n' "$compressable_systems_list") + done < <(printf '%s\n' "$compressible_systems_list") } From 35cb7a2df43a103c608bfa700874c3ae5b6f05ea Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 20 Mar 2025 15:34:23 -0400 Subject: [PATCH 27/52] Update Configurator single-file compression for new compress_game argument --- tools/configurator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index fadbc8ef..011df723 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -802,7 +802,7 @@ configurator_compress_single_game_dialog() { ( echo "# Compressing $(basename "$file") to $compatible_compression_format format" # This updates the Zenity dialog log i "Compressing $(basename "$file") to $compatible_compression_format format" - compress_game "$compatible_compression_format" "$file" "$system" + compress_game "$compatible_compression_format" "$file" "$post_compression_cleanup" "$system" ) | rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ From a432b48121cff44e314cd791894d44348c244588 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 20 Mar 2025 15:37:48 -0400 Subject: [PATCH 28/52] Update single-file CLI compression for new compress_game post-cleanup argument --- functions/compression.sh | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/functions/compression.sh b/functions/compression.sh index 956aa4b3..18f24201 100644 --- a/functions/compression.sh +++ b/functions/compression.sh @@ -220,22 +220,29 @@ cli_compress_single_game() { local file=$(realpath "$1") read -p "Do you want to have the original file removed after compression is complete? Please answer y/n and press Enter: " post_compression_cleanup read -p "RetroDECK will now attempt to compress your selected game. Press Enter key to continue..." - if [[ ! -z "$file" ]]; then - if [[ -f "$file" ]]; then - local system=$(echo "$file" | grep -oE "$roms_folder/[^/]+" | grep -oE "[^/]+$") - local compatible_compression_format=$(find_compatible_compression_format "$file") - if [[ ! $compatible_compression_format == "none" ]]; then - log i "$(basename "$file") can be compressed to $compatible_compression_format" - compress_game "$compatible_compression_format" "$file" "$system" + if [[ "$post_compression_cleanup" == "y" || "$post_compression_cleanup" == "n" ]]; then + if [[ ! -z "$file" ]]; then + if [[ -f "$file" ]]; then + local system=$(echo "$file" | grep -oE "$roms_folder/[^/]+" | grep -oE "[^/]+$") + local compatible_compression_format=$(find_compatible_compression_format "$file") + if [[ ! $compatible_compression_format == "none" ]]; then + log i "$(basename "$file") can be compressed to $compatible_compression_format" + if [[ "$post_compression_cleanup" == "y" ]]; then + post_compression_cleanup="true" + else + post_compression_cleanup="false" + fi + compress_game "$compatible_compression_format" "$file" "$post_compression_cleanup" "$system" + else + log w "$(basename "$file") does not have any compatible compression formats." + fi else - log w "$(basename "$file") does not have any compatible compression formats." + log w "File not found, please specify the full path to the file to be compressed." fi - else - log w "File not found, please specify the full path to the file to be compressed." - fi - else - log i "Please use this command format \"--compress-one \"" - fi + else + log i "Please use this command format \"--compress-one \"" + fi + fi } cli_compress_all_games() { From c768a5c623b7db6132a1aa3abdfbdd493266f461 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 20 Mar 2025 15:38:55 -0400 Subject: [PATCH 29/52] Add clarification if post-compression cleanup response was not valid --- functions/compression.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/functions/compression.sh b/functions/compression.sh index 18f24201..ad3db8aa 100644 --- a/functions/compression.sh +++ b/functions/compression.sh @@ -242,6 +242,8 @@ cli_compress_single_game() { else log i "Please use this command format \"--compress-one \"" fi + else + log i "The response for post-compression file cleanup was not correct. Please try again." fi } From 8575af8d42aaea4c2ebf19cad0fc300d44e7d910 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Thu, 20 Mar 2025 15:41:07 -0400 Subject: [PATCH 30/52] Update multi-file CLI compression for new compress_game post-compression cleanup argument --- functions/compression.sh | 50 +++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/functions/compression.sh b/functions/compression.sh index ad3db8aa..4482df47 100644 --- a/functions/compression.sh +++ b/functions/compression.sh @@ -219,8 +219,8 @@ cli_compress_single_game() { # USAGE: cli_compress_single_game $full_file_path local file=$(realpath "$1") read -p "Do you want to have the original file removed after compression is complete? Please answer y/n and press Enter: " post_compression_cleanup - read -p "RetroDECK will now attempt to compress your selected game. Press Enter key to continue..." - if [[ "$post_compression_cleanup" == "y" || "$post_compression_cleanup" == "n" ]]; then + if [[ "$post_compression_cleanup" == "y" || "$post_compression_cleanup" == "n" ]]; then + read -p "RetroDECK will now attempt to compress your selected game. Press Enter key to continue..." if [[ ! -z "$file" ]]; then if [[ -f "$file" ]]; then local system=$(echo "$file" | grep -oE "$roms_folder/[^/]+" | grep -oE "[^/]+$") @@ -265,25 +265,33 @@ cli_compress_all_games() { fi read -p "Do you want to have the original files removed after compression is complete? Please answer y/n and press Enter: " post_compression_cleanup - read -p "RetroDECK will now attempt to compress all compatible games. Press Enter key to continue..." - - while IFS= read -r system # Find and validate all games that are able to be compressed with this compression type - do - local compression_candidates=$(find "$roms_folder/$system" -type f -not -iname "*.txt") - if [[ ! -z "$compression_candidates" ]]; then - log i "Checking files for $system" - while IFS= read -r file - do - local compatible_compression_format=$(find_compatible_compression_format "$file") - if [[ ! "$compatible_compression_format" == "none" ]]; then - log i "$(basename "$file") can be compressed to $compatible_compression_format" - compress_game "$compatible_compression_format" "$file" "$system" - else - log w "No compatible compression format found for $(basename "$file")" - fi - done < <(printf '%s\n' "$compression_candidates") + if [[ "$post_compression_cleanup" == "y" || "$post_compression_cleanup" == "n" ]]; then + read -p "RetroDECK will now attempt to compress all compatible games. Press Enter key to continue..." + if [[ "$post_compression_cleanup" == "y" ]]; then + post_compression_cleanup="true" else - log w "No compatible files found for compression in $system" + post_compression_cleanup="false" fi - done < <(printf '%s\n' "$compressible_systems_list") + while IFS= read -r system # Find and validate all games that are able to be compressed with this compression type + do + local compression_candidates=$(find "$roms_folder/$system" -type f -not -iname "*.txt") + if [[ ! -z "$compression_candidates" ]]; then + log i "Checking files for $system" + while IFS= read -r file + do + local compatible_compression_format=$(find_compatible_compression_format "$file") + if [[ ! "$compatible_compression_format" == "none" ]]; then + log i "$(basename "$file") can be compressed to $compatible_compression_format" + compress_game "$compatible_compression_format" "$file" "$post_compression_cleanup" "$system" + else + log w "No compatible compression format found for $(basename "$file")" + fi + done < <(printf '%s\n' "$compression_candidates") + else + log w "No compatible files found for compression in $system" + fi + done < <(printf '%s\n' "$compressible_systems_list") + else + log i "The response for post-compression file cleanup was not correct. Please try again." + fi } From bc4ca01312380ed689da434912e91e8e08053fa0 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Mon, 24 Mar 2025 15:06:30 -0400 Subject: [PATCH 31/52] Static backups folder definition no longer needed --- functions/global.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/functions/global.sh b/functions/global.sh index 90c8519b..61b83b65 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -259,6 +259,5 @@ fi logs_folder="$rdhome/logs" # The path of the logs folder, here we collect all the logs steamsync_folder="$rdhome/.sync" # Folder containing all the steam sync launchers for SRM steamsync_folder_tmp="$rdhome/.sync-tmp" # Temp folder containing all the steam sync launchers for SRM -backups_folder="$rdhome/backups" # Folder containing all the RetroDECK backups export GLOBAL_SOURCED=true From 080030ec7a261e06acd29916c03d60e610b31d8c Mon Sep 17 00:00:00 2001 From: icenine451 Date: Mon, 24 Mar 2025 15:07:25 -0400 Subject: [PATCH 32/52] Static logs_folder definition no longer needed --- functions/global.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/functions/global.sh b/functions/global.sh index 61b83b65..53f2b54d 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -256,7 +256,6 @@ else multi_user_data_folder="$rdhome/multi-user-data" # The default location of multi-user environment profiles fi -logs_folder="$rdhome/logs" # The path of the logs folder, here we collect all the logs steamsync_folder="$rdhome/.sync" # Folder containing all the steam sync launchers for SRM steamsync_folder_tmp="$rdhome/.sync-tmp" # Temp folder containing all the steam sync launchers for SRM From 5da28562d4340124bb9d719a875658ff33c2ce5d Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 08:54:14 -0400 Subject: [PATCH 33/52] Consolidate steam_sync.sh sourcing --- functions/global.sh | 1 + functions/other_functions.sh | 1 - tools/configurator.sh | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/functions/global.sh b/functions/global.sh index 53f2b54d..12953e19 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -52,6 +52,7 @@ source /app/libexec/prepare_component.sh source /app/libexec/presets.sh source /app/libexec/configurator_functions.sh source /app/libexec/run_game.sh +source /app/libexec/steam_sync.sh # Static variables rd_conf="/var/config/retrodeck/retrodeck.cfg" # RetroDECK config file path diff --git a/functions/other_functions.sh b/functions/other_functions.sh index 61177835..b72ee124 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -1189,7 +1189,6 @@ quit_retrodeck() { # if steam sync is on do the magic if [[ $(get_setting_value "$rd_conf" "steam_sync" retrodeck "options") == "true" ]]; then ( - source /app/libexec/steam_sync.sh add_to_steam "$(ls "$rdhome/ES-DE/gamelists/")" ) | rd_zenity --progress \ diff --git a/tools/configurator.sh b/tools/configurator.sh index 011df723..d4bedb5e 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -1240,7 +1240,6 @@ enable_steam_sync() { disable_steam_sync() { set_setting_value "$rd_conf" "steam_sync" "false" retrodeck "options" - source /app/libexec/steam_sync.sh remove_from_steam zenity --icon-name=net.retrodeck.retrodeck --info --no-wrap --ok-label="OK" \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ From 72efc37248d214bcf38c93864babd8c1734ebafc Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 09:39:06 -0400 Subject: [PATCH 34/52] Move sanitize() function to other_functions.sh --- functions/other_functions.sh | 6 ++++++ functions/steam_sync.sh | 6 +----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/functions/other_functions.sh b/functions/other_functions.sh index b72ee124..53399191 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -1401,3 +1401,9 @@ repair_paths() { configurator_generic_dialog "RetroDECK Path Repair" "All RetroDECK folders were found at their expected locations." fi } + +# Function to sanitize strings for filenames +sanitize() { + # Replace sequences of underscores with a single space + echo "$1" | sed -e 's/_\{2,\}/ /g' -e 's/_/ /g' -e 's/:/ -/g' -e 's/&/and/g' -e 's%/%and%g' -e 's/ / /g' +} diff --git a/functions/steam_sync.sh b/functions/steam_sync.sh index 64a6077f..f94358c9 100644 --- a/functions/steam_sync.sh +++ b/functions/steam_sync.sh @@ -1,10 +1,6 @@ #!/bin/bash -# Function to sanitize strings for filenames -sanitize() { - # Replace sequences of underscores with a single space - echo "$1" | sed -e 's/_\{2,\}/ /g' -e 's/_/ /g' -e 's/:/ -/g' -e 's/&/and/g' -e 's%/%and%g' -e 's/ / /g' -} + add_to_steam() { From a121cffecb1c79b04f5bddd54c1667d979f75b7d Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 09:44:51 -0400 Subject: [PATCH 35/52] Update SRM-related global definitions --- functions/global.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/functions/global.sh b/functions/global.sh index 12953e19..24f08699 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -257,7 +257,9 @@ else multi_user_data_folder="$rdhome/multi-user-data" # The default location of multi-user environment profiles fi -steamsync_folder="$rdhome/.sync" # Folder containing all the steam sync launchers for SRM -steamsync_folder_tmp="$rdhome/.sync-tmp" # Temp folder containing all the steam sync launchers for SRM +# Steam ROM Manager user files and paths + +steamsync_folder="$rdhome/.sync" # Folder containing favorites manifest for SRM +retrodeck_favorites_file="$steamsync_folder/retrodeck_favorites.json" # The current SRM manifest of all games that have been favorited in ES-DE export GLOBAL_SOURCED=true From 04be8628df8fa50267398404105aff7b986d457b Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 11:00:35 -0400 Subject: [PATCH 36/52] Remove redundant call of populate_steamuser_srm which is already called within get_steam_user --- functions/prepare_component.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/functions/prepare_component.sh b/functions/prepare_component.sh index 26debdbe..08ad8357 100644 --- a/functions/prepare_component.sh +++ b/functions/prepare_component.sh @@ -138,8 +138,6 @@ prepare_component() { jq '.environmentVariables.romsDirectory = "'$rdhome'/.sync"' "$srm_userdata/userSettings.json" > "$srm_userdata/tmp.json" && mv -f "$srm_userdata/tmp.json" "$srm_userdata/userSettings.json" get_steam_user - populate_steamuser_srm - fi if [[ "$component" =~ ^(retroarch|all)$ ]]; then From dadd6c87e7efda751798f34137adcdd894d2a7f4 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 11:02:15 -0400 Subject: [PATCH 37/52] Update shipped SRM userConfigurations.json for new manifest sync type --- config/steam-rom-manager/userConfigurations.json | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/config/steam-rom-manager/userConfigurations.json b/config/steam-rom-manager/userConfigurations.json index 8f3afbaf..eb0da29b 100644 --- a/config/steam-rom-manager/userConfigurations.json +++ b/config/steam-rom-manager/userConfigurations.json @@ -1,6 +1,6 @@ [ { - "parserType": "Glob", + "parserType": "Manual", "configTitle": "RetroDECK Steam Sync", "steamDirectory": "${steamdirglobal}", "romDirectory": "${romsdirglobal}", @@ -8,11 +8,9 @@ "RetroDECK" ], "executableArgs": "", - "executableModifier": "\"${exePath}\"", + "executableModifier": "", "startInDirectory": "", "titleModifier": "${fuzzyTitle}", - "fetchControllerTemplatesButton": null, - "removeControllersButton": null, "steamInputEnabled": "2", "imageProviders": [ "sgdb", @@ -29,7 +27,7 @@ ] }, "parserInputs": { - "glob": "${title}.sh" + "manualManifests": "${romsdirglobal}" }, "executable": { "path": "", @@ -108,7 +106,6 @@ ], "sizes": [], "sizesHero": [], - "sizesTall": null, "sizesIcon": [] } }, @@ -256,4 +253,4 @@ "parserId": "173908444383456337", "version": 25 } -] \ No newline at end of file +] From ebb36f0b49041d9bc4bad13468770350fe443dbe Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 11:02:30 -0400 Subject: [PATCH 38/52] Add SRM log definition to global --- functions/global.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/global.sh b/functions/global.sh index 24f08699..db7d5d4e 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -261,5 +261,6 @@ fi steamsync_folder="$rdhome/.sync" # Folder containing favorites manifest for SRM retrodeck_favorites_file="$steamsync_folder/retrodeck_favorites.json" # The current SRM manifest of all games that have been favorited in ES-DE +srm_log="$logs_folder/srm_log.log" # Log file for capturing the output of the most recent SRM run, for debugging purposes export GLOBAL_SOURCED=true From 94c066511e0ff51a52c7f3597c7714b9245ca2ab Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 11:03:10 -0400 Subject: [PATCH 39/52] Update steam_sync to new SRM manifest utilization --- functions/steam_sync.sh | 157 ++++++++++++++++------------------------ 1 file changed, 64 insertions(+), 93 deletions(-) diff --git a/functions/steam_sync.sh b/functions/steam_sync.sh index f94358c9..ccd778be 100644 --- a/functions/steam_sync.sh +++ b/functions/steam_sync.sh @@ -1,21 +1,33 @@ #!/bin/bash +steam_sync() { - -add_to_steam() { + # This function looks for favorited games in all ES-DE gamelists and builds a manifest of any found. + # It then compares the new manifest to the existing one (if it exists) and runs an SRM sync if there are differences + # If all favorites were removed from ES-DE, it will remove all existing entries from Steam and then remove the favorites manifest entirely + # If there is no existing manifest, this is a first time sync and games are synced automatically + # USAGE: steam_sync log "i" "Starting Steam Sync" - create_dir $steamsync_folder - create_dir $steamsync_folder_tmp - local srm_path="/var/config/steam-rom-manager/userData/userConfigurations.json" if [ ! -f "$srm_path" ]; then log "e" "Steam ROM Manager configuration not initialized! Initializing now." prepare_component "reset" "steam-rom-manager" fi - # Iterate through all gamelist.xml files in the folder structure + # Prepare fresh log file + echo > "$srm_log" + + # Prepare new favorites manifest + echo "[]" > "${retrodeck_favorites_file}.new" # Initialize favorites JSON file + favorites_found="false" + + # Static definitions for all JSON objects + target="flatpak" + launch_command="run net.retrodeck.retrodeck" + startIn="" + for system_path in "$rdhome/ES-DE/gamelists/"*/; do # Skip the CLEANUP folder if [[ "$system_path" == *"/CLEANUP/"* ]]; then @@ -23,95 +35,54 @@ add_to_steam() { fi system=$(basename "$system_path") # Extract the folder name as the system name gamelist="${system_path}gamelist.xml" - - log d "Reading favorites for $system" - - # Ensure gamelist.xml exists in the current folder - if [ -f "$gamelist" ]; then - while IFS= read -r line; do - # Detect the start of a block - if [[ "$line" =~ \ ]]; then - to_be_added=false # Reset the flag for a new block - path="" - name="" - fi - - # Check for true - if [[ "$line" =~ \true\<\/favorite\> ]]; then - to_be_added=true - fi - - # Extract the and remove leading "./" if present - if [[ "$line" =~ \(.*)\<\/path\> ]]; then - path="${BASH_REMATCH[1]#./}" - fi - - # Extract and sanitize - if [[ "$line" =~ \(.*)\<\/name\> ]]; then - name=$(sanitize "${BASH_REMATCH[1]}") - fi - - # Detect the end of a block - if [[ "$line" =~ \<\/game\> ]]; then - # If the block is meaningful (marked as favorite), generate the launcher - if [ "$to_be_added" = true ] && [ -n "$path" ] && [ -n "$name" ]; then - local launcher="$steamsync_folder/${name}.sh" - local launcher_tmp="$steamsync_folder_tmp/${name}.sh" - - # Create the launcher file - # Check if the launcher file does not already exist - if [ ! -e "$launcher_tmp" ]; then - log d "Creating launcher file: $launcher" - command="flatpak run net.retrodeck.retrodeck -s $system '$roms_folder/$system/$path'" - echo '#!/bin/bash' > "$launcher_tmp" - echo "$command" >> "$launcher_tmp" - chmod +x "$launcher_tmp" - else - log d "$(basename "$launcher") desktop file already exists" - fi - fi - - # Clean up variables for safety - to_be_added=false - path="" - name="" - fi - done < "$gamelist" - else - log "e" "Gamelist file not found for system: $system" - fi + system_favorites=$(xml sel -t -m "//game[favorite='true']" -v "path" -n "$gamelist") + while read -r game; do + if [[ -n "$game" ]]; then # Avoid empty lines created by xmlstarlet + favorites_found="true" + local game="${game#./}" # Remove leading ./ + # Construct launch options with the rom path in quotes, to handle spaces + local launchOptions="$launch_command -s $system \"$roms_folder/$system/$game\"" + jq --arg title "${game%.*}" --arg target "$target" --arg launchOptions "$launchOptions" \ + '. += [{"title": $title, "target": $target, "launchOptions": $launchOptions}]' "${retrodeck_favorites_file}.new" > "${retrodeck_favorites_file}.tmp" \ + && mv "${retrodeck_favorites_file}.tmp" "${retrodeck_favorites_file}.new" + fi + done <<< "$system_favorites" done - # Remove the old Steam sync folder - rm -rf "$steamsync_folder" - - # Move the temporary Steam sync folder to the final location - log d "Moving the temporary Steam sync folder to the final location" - mv "$steamsync_folder_tmp" "$steamsync_folder" && log d "\"$steamsync_folder_tmp\" -> \"$steamsync_folder\"" + if [[ -f "$retrodeck_favorites_file" ]]; then # If an existing favorites manifest exists + if [[ $favorites_found == "false" ]]; then # If no favorites were found in the gamelists + log i "No favorites were found in current ES-DE gamelists, removing old entries" + # Remove old entries + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 + # Old manifest cleanup + rm "$retrodeck_favorites_file" + rm "${retrodeck_favorites_file}.new" + else + if cmp -s "$retrodeck_favorites_file" "${retrodeck_favorites_file}.new"; then # See if the favorites manifests are the same, meaning there were no changes + log i "ES-DE favorites have not changed, no need to sync again" + rm "${retrodeck_favorites_file}.new" + else + log d "New and old manifests are different, running sync" + # Remove old entries + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 - # Check if the Steam sync folder is empty - if [ -z "$(ls -A $steamsync_folder)" ]; then - # if empty, add the remove_from_steam function - log d "No games found, cleaning shortcut" - remove_from_steam - else - log d "Updating game list" - steam-rom-manager enable --names "RetroDECK Steam Sync" - steam-rom-manager add + # Load new favorites manifest + mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" + + # Add new favorites manifest + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager add >> "$srm_log" 2>&1 + fi + fi + elif [[ $favorites_found == "true" ]]; then # Only sync if some favorites were found + log d "First time building favorites manifest, running sync" + mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" + # Add new favorites manifest + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager add >> "$srm_log" 2>&1 fi } - -# Function to remove the games from Steam, this is a workaround to make SRM remove the games as it cannot remove the games based on a empty folder -# So a dummy file must be in place to make SRM remove the other games -remove_from_steam() { - log d "Creating dummy game" - cat "" > "$steamsync_folder/CUL0.sh" - log d "Cleaning the shortcut" - steam-rom-manager enable --names "RetroDECK Steam Sync" - steam-rom-manager disable --names "RetroDECK Launcher" - steam-rom-manager remove - log d "Removing dummy game" - rm "$steamsync_folder/CUL0.sh" - steam-rom-manager enable --names "RetroDECK Launcher" - steam-rom-manager disable --names "RetroDECK Steam Sync" -} From c7ff340b8241027253936ead6dcc4b1291cf4dee Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 11:43:59 -0400 Subject: [PATCH 40/52] Update quit_retrodeck for new steam_sync --- functions/other_functions.sh | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/functions/other_functions.sh b/functions/other_functions.sh index 53399191..cc300053 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -1187,21 +1187,8 @@ quit_retrodeck() { pkill -f "es-de" # if steam sync is on do the magic - if [[ $(get_setting_value "$rd_conf" "steam_sync" retrodeck "options") == "true" ]]; then - ( - add_to_steam "$(ls "$rdhome/ES-DE/gamelists/")" - ) | - rd_zenity --progress \ - --title="Syncing with Steam" \ - --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ - --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ - --percentage=25 \ - --pulsate \ - --width=500 \ - --height=150 \ - --auto-close \ - --auto-kill \ - --no-cancel + if [[ $(get_setting_value "$rd_conf" "steam_sync" "retrodeck" "options") == "true" ]]; then + steam_sync fi log i "Shutting down RetroDECK's framework" pkill -f "retrodeck" From 4042955863d5696961bad125b452baf69dafa93a Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 11:44:51 -0400 Subject: [PATCH 41/52] Move Steam Sync zenity dialogs to steam_sync function so they don't show unnecessarily --- functions/steam_sync.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/functions/steam_sync.sh b/functions/steam_sync.sh index ccd778be..e84a5263 100644 --- a/functions/steam_sync.sh +++ b/functions/steam_sync.sh @@ -52,10 +52,17 @@ steam_sync() { if [[ -f "$retrodeck_favorites_file" ]]; then # If an existing favorites manifest exists if [[ $favorites_found == "false" ]]; then # If no favorites were found in the gamelists log i "No favorites were found in current ES-DE gamelists, removing old entries" + ( # Remove old entries steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 steam-rom-manager remove >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="Syncing with Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ + --pulsate --width=500 --height=150 --auto-close --no-cancel # Old manifest cleanup rm "$retrodeck_favorites_file" rm "${retrodeck_favorites_file}.new" @@ -65,6 +72,7 @@ steam_sync() { rm "${retrodeck_favorites_file}.new" else log d "New and old manifests are different, running sync" + ( # Remove old entries steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 @@ -76,13 +84,26 @@ steam_sync() { # Add new favorites manifest steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager add >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="Syncing with Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ + --pulsate --width=500 --height=150 --auto-close --no-cancel fi fi elif [[ $favorites_found == "true" ]]; then # Only sync if some favorites were found log d "First time building favorites manifest, running sync" mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" + ( # Add new favorites manifest steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager add >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="Syncing with Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ + --pulsate --width=500 --height=150 --auto-close --no-cancel fi } From 75103a21c21d9cbbcf58013f6123b39c7893c36c Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 11:45:20 -0400 Subject: [PATCH 42/52] Update Zenity Configurator for new steam_sync --- tools/configurator.sh | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index d4bedb5e..498c6ec7 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -1210,7 +1210,7 @@ configurator_steam_sync() { if [ $? == 0 ] # User clicked "Yes" then - disable_steam_sync + configurator_disable_steam_sync else # User clicked "Cancel" configurator_welcome_dialog fi @@ -1222,15 +1222,16 @@ configurator_steam_sync() { if [ $? == 0 ] then - enable_steam_sync + configurator_enable_steam_sync else configurator_welcome_dialog fi fi } -enable_steam_sync() { +configurator_enable_steam_sync() { set_setting_value "$rd_conf" "steam_sync" "true" retrodeck "options" + steam_sync zenity --icon-name=net.retrodeck.retrodeck --info --no-wrap --ok-label="OK" \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Configurator - RetroDECK Steam Syncronization" \ @@ -1238,9 +1239,22 @@ enable_steam_sync() { configurator_welcome_dialog } -disable_steam_sync() { +configurator_disable_steam_sync() { set_setting_value "$rd_conf" "steam_sync" "false" retrodeck "options" - remove_from_steam + # Remove only synced favorites, leave RetroDECK shortcut if it exists + ( + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="Removing RetroDECK Sync from Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="Removing synced entries from Steam, please wait..." \ + --pulsate --width=500 --height=150 --auto-close --no-cancel + if [[ -f "$retrodeck_favorites_file" ]]; then + rm -f "$retrodeck_favorites_file" + fi zenity --icon-name=net.retrodeck.retrodeck --info --no-wrap --ok-label="OK" \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Configurator - RetroDECK Steam Syncronization" \ From 0f4357f4d42226d11583353adc856f407217f3cc Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 12:10:02 -0400 Subject: [PATCH 43/52] Update Zenity Configurator "Add RetroDECK to Steam" for new steam_sync --- tools/configurator.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index 498c6ec7..8110560a 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -576,7 +576,16 @@ configurator_tools_dialog() { ;; "Add RetroDECK to Steam" ) - add_retrodeck_to_steam + ( + # Add RetroDECK launcher to Steam + steam-rom-manager enable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager add >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="RetroDECK Configurator: Add RetroDECK to Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="Adding RetroDECK launcher to Steam, please wait..." \ + --pulsate --width=500 --height=150 --auto-close --no-cancel configurator_tools_dialog ;; From 26baed91f73f193ffd44b4345892275a5ed60394 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 12:17:29 -0400 Subject: [PATCH 44/52] Update steam_sync for proper Configurator detection and dialogs --- functions/steam_sync.sh | 101 +++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 37 deletions(-) diff --git a/functions/steam_sync.sh b/functions/steam_sync.sh index e84a5263..2605d4c3 100644 --- a/functions/steam_sync.sh +++ b/functions/steam_sync.sh @@ -52,17 +52,24 @@ steam_sync() { if [[ -f "$retrodeck_favorites_file" ]]; then # If an existing favorites manifest exists if [[ $favorites_found == "false" ]]; then # If no favorites were found in the gamelists log i "No favorites were found in current ES-DE gamelists, removing old entries" - ( - # Remove old entries - steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 - steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 - steam-rom-manager remove >> "$srm_log" 2>&1 - ) | - rd_zenity --progress \ - --title="Syncing with Steam" \ - --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ - --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ - --pulsate --width=500 --height=150 --auto-close --no-cancel + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + ( + # Remove old entries + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="Syncing with Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ + --pulsate --width=500 --height=150 --auto-close --no-cancel + else + # Remove old entries + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 + fi # Old manifest cleanup rm "$retrodeck_favorites_file" rm "${retrodeck_favorites_file}.new" @@ -72,38 +79,58 @@ steam_sync() { rm "${retrodeck_favorites_file}.new" else log d "New and old manifests are different, running sync" - ( - # Remove old entries - steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 - steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 - steam-rom-manager remove >> "$srm_log" 2>&1 + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + ( + # Remove old entries + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 - # Load new favorites manifest - mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" + # Load new favorites manifest + mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" - # Add new favorites manifest - steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 - steam-rom-manager add >> "$srm_log" 2>&1 - ) | - rd_zenity --progress \ - --title="Syncing with Steam" \ - --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ - --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ - --pulsate --width=500 --height=150 --auto-close --no-cancel + # Add new favorites manifest + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager add >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="Syncing with Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ + --pulsate --width=500 --height=150 --auto-close --no-cancel + else + # Remove old entries + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 + + # Load new favorites manifest + mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" + + # Add new favorites manifest + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager add >> "$srm_log" 2>&1 + fi fi fi elif [[ $favorites_found == "true" ]]; then # Only sync if some favorites were found log d "First time building favorites manifest, running sync" mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" - ( - # Add new favorites manifest - steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 - steam-rom-manager add >> "$srm_log" 2>&1 - ) | - rd_zenity --progress \ - --title="Syncing with Steam" \ - --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ - --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ - --pulsate --width=500 --height=150 --auto-close --no-cancel + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + ( + # Add new favorites manifest + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager add >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="Syncing with Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ + --pulsate --width=500 --height=150 --auto-close --no-cancel + else + # Add new favorites manifest + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager add >> "$srm_log" 2>&1 + fi fi } From 6b7b5c28c2deb2525deb9980983727811250411b Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 16:19:59 -0400 Subject: [PATCH 45/52] Add retrodeck_removed_favorites definition to global.sh --- functions/global.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/global.sh b/functions/global.sh index db7d5d4e..8948b352 100644 --- a/functions/global.sh +++ b/functions/global.sh @@ -262,5 +262,6 @@ fi steamsync_folder="$rdhome/.sync" # Folder containing favorites manifest for SRM retrodeck_favorites_file="$steamsync_folder/retrodeck_favorites.json" # The current SRM manifest of all games that have been favorited in ES-DE srm_log="$logs_folder/srm_log.log" # Log file for capturing the output of the most recent SRM run, for debugging purposes +retrodeck_removed_favorites="$steamsync_folder/retrodeck_removed_favorites.json" # Temporary manifest of any games that were unfavorited in ES-DE and should be removed from Steam export GLOBAL_SOURCED=true From 31ee16720227aa2f2f529798393d5ecd4d0dc94e Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 16:22:17 -0400 Subject: [PATCH 46/52] Upgrade steam_sync to better handle unfavorited games. Previously entire SRM manifest would have been hard synced (remove everything, add current set of favorites), now will only remove specifically unfavorited items and add all new ones --- functions/steam_sync.sh | 89 +++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 29 deletions(-) diff --git a/functions/steam_sync.sh b/functions/steam_sync.sh index 2605d4c3..5a34e290 100644 --- a/functions/steam_sync.sh +++ b/functions/steam_sync.sh @@ -21,7 +21,6 @@ steam_sync() { # Prepare new favorites manifest echo "[]" > "${retrodeck_favorites_file}.new" # Initialize favorites JSON file - favorites_found="false" # Static definitions for all JSON objects target="flatpak" @@ -33,63 +32,101 @@ steam_sync() { if [[ "$system_path" == *"/CLEANUP/"* ]]; then continue fi + # Skip folders with no gamelists + if [[ ! -f "${system_path}gamelist.xml" ]]; then + continue + fi system=$(basename "$system_path") # Extract the folder name as the system name gamelist="${system_path}gamelist.xml" system_favorites=$(xml sel -t -m "//game[favorite='true']" -v "path" -n "$gamelist") while read -r game; do if [[ -n "$game" ]]; then # Avoid empty lines created by xmlstarlet - favorites_found="true" local game="${game#./}" # Remove leading ./ - # Construct launch options with the rom path in quotes, to handle spaces - local launchOptions="$launch_command -s $system \"$roms_folder/$system/$game\"" - jq --arg title "${game%.*}" --arg target "$target" --arg launchOptions "$launchOptions" \ - '. += [{"title": $title, "target": $target, "launchOptions": $launchOptions}]' "${retrodeck_favorites_file}.new" > "${retrodeck_favorites_file}.tmp" \ - && mv "${retrodeck_favorites_file}.tmp" "${retrodeck_favorites_file}.new" + if [[ -f "$roms_folder/$system/$game" ]]; then # Validate file exists and isn't a stale ES-DE entry for a removed file + # Construct launch options with the rom path in quotes, to handle spaces + local launchOptions="$launch_command -s $system \"$roms_folder/$system/$game\"" + jq --arg title "${game%.*}" --arg target "$target" --arg launchOptions "$launchOptions" \ + '. += [{"title": $title, "target": $target, "launchOptions": $launchOptions}]' "${retrodeck_favorites_file}.new" > "${retrodeck_favorites_file}.tmp" \ + && mv "${retrodeck_favorites_file}.tmp" "${retrodeck_favorites_file}.new" + fi fi done <<< "$system_favorites" done + if [[ -f "$retrodeck_favorites_file" && -f "${retrodeck_favorites_file}.new" ]]; then + # Look for favorites removed between steam_sync runs, if any + removed_items=$(jq -n \ + --slurpfile source "$retrodeck_favorites_file" \ + --slurpfile target "${retrodeck_favorites_file}.new" \ + '[$source[0][] | select(. as $item | ($target[0] | map(. == $item) | any | not))]') + fi + + # Check if there are any missing objects + if [[ "$(echo "$removed_items" | jq 'length')" -gt 0 ]]; then + log d "Some favorites were removed between sync, writing to $retrodeck_removed_favorites" + echo "$removed_items" > "$retrodeck_removed_favorites" + fi + + # Decide if sync needs to happen if [[ -f "$retrodeck_favorites_file" ]]; then # If an existing favorites manifest exists - if [[ $favorites_found == "false" ]]; then # If no favorites were found in the gamelists + if [[ ! "$(cat "${retrodeck_favorites_file}.new" | jq 'length')" -gt 0 ]]; then # If all favorites were removed from all gamelists, meaning new manifest is empty log i "No favorites were found in current ES-DE gamelists, removing old entries" if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then ( # Remove old entries - steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager remove >> "$srm_log" 2>&1 ) | rd_zenity --progress \ --title="Syncing with Steam" \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ - --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ + --text="\t\t\t\tRemoving unfavorited games from Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ --pulsate --width=500 --height=150 --auto-close --no-cancel else # Remove old entries - steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager remove >> "$srm_log" 2>&1 fi # Old manifest cleanup rm "$retrodeck_favorites_file" rm "${retrodeck_favorites_file}.new" - else + else # The new favorites manifest is not empty if cmp -s "$retrodeck_favorites_file" "${retrodeck_favorites_file}.new"; then # See if the favorites manifests are the same, meaning there were no changes log i "ES-DE favorites have not changed, no need to sync again" rm "${retrodeck_favorites_file}.new" else log d "New and old manifests are different, running sync" + if [[ -f "$retrodeck_removed_favorites" ]]; then # If some favorites were removed between syncs + log d "Some favorites removed between syncs, removing unfavorited games" + # Load removed favorites as manifest and run SRM remove + mv "$retrodeck_removed_favorites" "$retrodeck_favorites_file" + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + ( + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="Syncing with Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="\t\t\t\tRemoving unfavorited games from Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ + --pulsate --width=500 --height=150 --auto-close --no-cancel + else + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 + steam-rom-manager remove >> "$srm_log" 2>&1 + fi + fi + + # Load new favorites manifest as games to add during sync + mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then ( - # Remove old entries - steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 - steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 - steam-rom-manager remove >> "$srm_log" 2>&1 - - # Load new favorites manifest - mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" - # Add new favorites manifest + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager add >> "$srm_log" 2>&1 ) | @@ -99,26 +136,19 @@ steam_sync() { --text="\t\t\t\tSyncing favorite games with Steam\n\nNOTE: This operation may take some time depending on the size of your library.\nFeel free to leave this in the background and switch to another application.\n\n" \ --pulsate --width=500 --height=150 --auto-close --no-cancel else - # Remove old entries - steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 - steam-rom-manager remove >> "$srm_log" 2>&1 - - # Load new favorites manifest - mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" - - # Add new favorites manifest steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager add >> "$srm_log" 2>&1 fi fi fi - elif [[ $favorites_found == "true" ]]; then # Only sync if some favorites were found + elif [[ "$(cat "${retrodeck_favorites_file}.new" | jq 'length')" -gt 0 ]]; then # No existing favorites manifest was found, so check if new manifest has entries log d "First time building favorites manifest, running sync" mv "${retrodeck_favorites_file}.new" "$retrodeck_favorites_file" if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then ( # Add new favorites manifest + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager add >> "$srm_log" 2>&1 ) | @@ -129,6 +159,7 @@ steam_sync() { --pulsate --width=500 --height=150 --auto-close --no-cancel else # Add new favorites manifest + steam-rom-manager disable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 steam-rom-manager enable --names "RetroDECK Steam Sync" >> "$srm_log" 2>&1 steam-rom-manager add >> "$srm_log" 2>&1 fi From b2cd96b260234d751331787bf4b609848b25a7f5 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 16:44:33 -0400 Subject: [PATCH 47/52] Upgrade backup_retrodeck_userdata with new "core" backup option, which will only include irreplaceable user data. Custom selections and a complete backup remain as options. --- functions/other_functions.sh | 63 +++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/functions/other_functions.sh b/functions/other_functions.sh index cc300053..387b2b27 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -355,11 +355,13 @@ update_vita3k_firmware() { backup_retrodeck_userdata() { # This function can compress one or more RetroDECK userdata folders into a single zip file for backup. - # The function can do a "standard" backup of all the normal userdata files (which can be very big if there is a lot of media) or a "custom" backup of only specified paths + # The function can do a "complete" backup of all userdata including ROMs and ES-DE media, so can end up being very large. + # The function can also do a "core" backup of all the very important userdata files (like saves, states and gamelists) or a "custom" backup of only specified paths # The function can take both folder names as defined in retrodeck.cfg or full paths as arguments for folders to backup # It will also validate that all the provided paths exist and that there is enough free space to perform the backup before actually proceeding. # It will also rotate backups so that there are only 3 maximum of each type (standard or custom) - # USAGE: backup_retrodeck_userdata standard + # USAGE: backup_retrodeck_userdata complete + # backup_retrodeck_userdata core # backup_retrodeck_userdata custom saves_folder states_folder /some/other/path create_dir "$backups_folder" @@ -368,7 +370,7 @@ backup_retrodeck_userdata() { backup_log_file="$logs_folder/${backup_date}_${backup_type}_backup_log.log" # Check if first argument is the type - if [[ "$1" == "standard" || "$1" == "custom" ]]; then + if [[ "$1" == "complete" || "$1" == "core" || "$1" == "custom" ]]; then backup_type="$1" shift # Remove the first argument else @@ -395,7 +397,7 @@ backup_retrodeck_userdata() { done < <(grep -v '^\s*$' $rd_conf | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f') # Determine which paths to backup - if [[ "$backup_type" == "standard" ]]; then + if [[ "$backup_type" == "complete" ]]; then for folder_name in "${!config_paths[@]}"; do path_value="${config_paths[$folder_name]}" if [[ -e "$path_value" ]]; then @@ -446,6 +448,59 @@ backup_retrodeck_userdata() { return 1 fi + elif [[ "$backup_type" == "core" ]]; then + for folder_name in "${!config_paths[@]}"; do + if [[ $folder_name =~ (saves_folder|states_folder|logs_folder|screenshots_folder) ]]; then # Only include these paths + path_value="${config_paths[$folder_name]}" + if [[ -e "$path_value" ]]; then + paths_to_backup+=("$path_value") + log i "Adding to backup: $folder_name = $path_value" + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The $folder_name was not found at its expected location, $path_value\nSomething may be wrong with your RetroDECK installation." + fi + log i "Warning: Path does not exist: $folder_name = $path_value" + fi + fi + done + + # Add static paths not defined in retrodeck.cfg + if [[ -e "$rdhome/ES-DE/collections" ]]; then + paths_to_backup+=("$rdhome/ES-DE/collections") + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE collections folder was not found at its expected location, $rdhome/ES-DE/collections\nSomething may be wrong with your RetroDECK installation." + fi + log i "Warning: Path does not exist: ES-DE/collections = $rdhome/ES-DE/collections" + fi + + if [[ -e "$rdhome/ES-DE/gamelists" ]]; then + paths_to_backup+=("$rdhome/ES-DE/gamelists") + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE gamelists folder was not found at its expected location, $rdhome/ES-DE/gamelists\nSomething may be wrong with your RetroDECK installation." + fi + log i "Warning: Path does not exist: ES-DE/gamelists = $rdhome/ES-DE/gamelists" + fi + + if [[ -e "$rdhome/ES-DE/custom_systems" ]]; then + paths_to_backup+=("$rdhome/ES-DE/custom_systems") + else + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE custom_systems folder was not found at its expected location, $rdhome/ES-DE/custom_systems\nSomething may be wrong with your RetroDECK installation." + fi + log i "Warning: Path does not exist: ES-DE/custom_systems = $rdhome/ES-DE/custom_systems" + fi + + # Check if we found any valid paths + if [[ ${#paths_to_backup[@]} -eq 0 ]]; then + if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then + configurator_generic_dialog "RetroDECK Userdata Backup" "No valid userdata folders were found.\nSomething may be wrong with your RetroDECK installation." + fi + log e "Error: No valid paths found in config file" + return 1 + fi + elif [[ "$backup_type" == "custom" ]]; then if [[ "$#" -eq 0 ]]; then # Check if any paths were provided in the arguments if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then From e1172262df3d4da56482cf8859a5b53df5959886 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 16:45:21 -0400 Subject: [PATCH 48/52] Ensure Zenity dialogs are shown when calling steam_sync from Zenity Configurator --- tools/configurator.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/configurator.sh b/tools/configurator.sh index 8110560a..9a7cba9d 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -1240,6 +1240,7 @@ configurator_steam_sync() { configurator_enable_steam_sync() { set_setting_value "$rd_conf" "steam_sync" "true" retrodeck "options" + export CONFIGURATOR_GUI="zenity" steam_sync zenity --icon-name=net.retrodeck.retrodeck --info --no-wrap --ok-label="OK" \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ From 52b34c41f2863ec62dd552fa2c34755132077abd Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 16:46:52 -0400 Subject: [PATCH 49/52] Add "core" backup option to pre-update choices. Start 0.9.2b post-update actions, including Steam Sync refresh. --- functions/post_update.sh | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/functions/post_update.sh b/functions/post_update.sh index 0d1f3c84..edd583c9 100644 --- a/functions/post_update.sh +++ b/functions/post_update.sh @@ -9,16 +9,22 @@ post_update() { update_rd_conf + export CONFIGURATOR_GUI="zenity" + # Optional userdata backup prior to update - choice=$(rd_zenity --title "RetroDECK Update - Backup Userdata" --info --no-wrap --ok-label="No Backup" --extra-button="Backup Some Userdata" --extra-button="Backup All Userdata" \ - --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --text="Would you like to backup some or all of the RetroDECK userdata prior to update?\n\nIf you choose \"Backup Some Userdata\" you will be given a choice of which folders to backup.\nPLEASE NOTE: A full backup may take up a large amount of space, especially if you have a lot of scraped media.") + choice=$(rd_zenity --title "RetroDECK Update - Backup Userdata" --info --no-wrap --ok-label="No Backup" --extra-button"Backup Core Userdata" --extra-button="Backup Some Userdata" --extra-button="Backup All Userdata" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --text="Would you like to backup some or all of the RetroDECK userdata prior to update?\n\nIf you choose \"Backup Core Userdata\" only irreplaceable files (like saves, states and gamelists) will be backed up. If you choose \"Backup Some Userdata\" you will be given a choice of which folders to backup.\n\nIf you choose \"Backup All Userdata\" then ALL data (including ROMs and downloaded media) will be backed up.\nPLEASE NOTE: A full backup may take up a large amount of space, especially if you have a lot of scraped media.") local rc=$? if [[ $rc == "0" ]] && [[ -z "$choice" ]]; then # User selected No Backup button log i "User chose to not backup prior to update." else case $choice in + "Backup Core Userdata" ) + log i "User chose to backup core userdata prior to update." + backup_retrodeck_userdata "core" + ;; "Backup Some Userdata" ) log i "User chose to backup some userdata prior to update." while read -r config_line; do @@ -44,13 +50,11 @@ post_update() { choices=() # Expand choice string into passable array IFS='^' read -ra choices <<< "$choice" - export CONFIGURATOR_GUI="zenity" backup_retrodeck_userdata "custom" "${choices[@]}" # Expand array of choices into individual arguments ;; "Backup All Userdata" ) log i "User chose to backup all userdata prior to update." - export CONFIGURATOR_GUI="zenity" - backup_retrodeck_userdata "standard" + backup_retrodeck_userdata "complete" ;; esac fi @@ -722,6 +726,29 @@ post_update() { fi # end of 0.9.1b + if [[ $(check_version_is_older_than "$version_being_updated" "0.9.2b") == "true" ]]; then + # In version 0.9.2b, the following changes were made that required config file updates/reset or other changes to the filesystem: + # Steam Sync completely rebuilt into new manifest system. Favorites will need to be nuked and, if steam_sync is enabled will be rebuilt. + + if [[ -d "$steamsync_folder" ]]; then # If Steam Sync has ever been run + steam-rom-manager nuke + steam_sync + if [[ "$(configurator_generic_question_dialog "RetroDECK 0.9.2b Steam Sync Reset" "In RetroDECK 0.9.2b we upgraded our Steam Sync feature and the shortcuts in Steam need to be rebuilt.\n\nAll of your ES-DE favorites are still unchanged.\nAny games you have favorited now will be recreated.\n\nIf you have added RetroDECK to Steam through our Configurator it will also be removed through this process.\nWould you like to add the RetroDECK shortcut again?")" == "true" ]]; then + ( + # Add RetroDECK launcher to Steam + steam-rom-manager enable --names "RetroDECK Launcher" >> "$srm_log" 2>&1 + steam-rom-manager add >> "$srm_log" 2>&1 + ) | + rd_zenity --progress \ + --title="RetroDECK Configurator: Add RetroDECK to Steam" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ + --text="Adding RetroDECK launcher to Steam, please wait..." \ + --pulsate --width=500 --height=150 --auto-close --no-cancel + fi + fi + + fi # end of 0.9.2b + # The following commands are run every time. if [[ -d "/var/data/dolphin-emu/Load/DynamicInputTextures" ]]; then # Refresh installed textures if they have been enabled @@ -758,5 +785,7 @@ post_update() { changelog_dialog "$version" fi + unset CONFIGURATOR_GUI + log i "Upgrade process completed successfully." } From 27a07fcb7011f4af8d7a5cc8b0703e519369f5e2 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 17:09:41 -0400 Subject: [PATCH 50/52] Fix issue where files were incorrectly compressed when only compressing some files --- tools/configurator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index 9a7cba9d..8d59cf46 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -874,7 +874,7 @@ configurator_compress_multiple_games_dialog() { if [[ $rc == 0 && -n "$choice" ]]; then while IFS="^" read -r game comp; do # Split Zenity choice string into compatible pairs (game^format) games_to_compress+=("$game"^"$comp") - done < "$compressible_games_list_file" + done <<< "$choice" elif [[ -n "$choice" ]]; then games_to_compress=("${all_compressible_games[@]}") else From 994294d62a27d04311cfa11e56b0e36e724ceb00 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 17:26:07 -0400 Subject: [PATCH 51/52] Remove screenshots_folder from core userdata backup --- functions/other_functions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/other_functions.sh b/functions/other_functions.sh index 387b2b27..f1878e27 100644 --- a/functions/other_functions.sh +++ b/functions/other_functions.sh @@ -450,7 +450,7 @@ backup_retrodeck_userdata() { elif [[ "$backup_type" == "core" ]]; then for folder_name in "${!config_paths[@]}"; do - if [[ $folder_name =~ (saves_folder|states_folder|logs_folder|screenshots_folder) ]]; then # Only include these paths + if [[ $folder_name =~ (saves_folder|states_folder|logs_folder) ]]; then # Only include these paths path_value="${config_paths[$folder_name]}" if [[ -e "$path_value" ]]; then paths_to_backup+=("$path_value") From 803d17eb7538f9e26d1ddf6bf9ae666ebff1caf9 Mon Sep 17 00:00:00 2001 From: icenine451 Date: Tue, 25 Mar 2025 17:26:32 -0400 Subject: [PATCH 52/52] Update Zenity Configurator for new backup function arguments --- tools/configurator.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/configurator.sh b/tools/configurator.sh index 8d59cf46..0b694561 100755 --- a/tools/configurator.sh +++ b/tools/configurator.sh @@ -465,15 +465,21 @@ configurator_tools_dialog() { log i "Configurator: opening \"$choice\" menu" configurator_generic_dialog "RetroDECK Configurator - Backup Userdata" "This tool will compress one or more RetroDECK userdata folders into a single zip file.\n\nThis process can take several minutes, and the resulting zip file can be found in the ~/retrodeck/backups folder." - choice=$(rd_zenity --title "RetroDECK Configurator Utility - Backup Userdata" --info --no-wrap --ok-label="Cancel" --extra-button="Backup Some Userdata" --extra-button="Backup All Userdata" \ - --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --text="Would you like to compress all RetroDECK userdata folders, or only some of them?") + choice=$(rd_zenity --title "RetroDECK Configurator Utility - Backup Userdata" --info --no-wrap --ok-label="Cancel" --extra-button"Backup Core Userdata" --extra-button="Backup Some Userdata" --extra-button="Backup All Userdata" \ + --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --text="Would you like to compress all RetroDECK userdata folders, or only some of them?\nThe \"Core Userdata\" includes irreplaceable files such as saves, states and gamelists.\n\n A complete \"All Userdata\" backup can take up A LOT of space if you have a large library.") local rc=$? if [[ $rc == "0" ]] && [[ -z "$choice" ]]; then # User selected Cancel button configurator_tools_dialog else case $choice in + "Backup Core Userdata" ) + log i "User chose to backup core userdata prior to update." + export CONFIGURATOR_GUI="zenity" + backup_retrodeck_userdata "core" + ;; "Backup Some Userdata" ) + log i "User chose to backup custom userdata prior to update." while read -r config_line; do local current_setting_name=$(get_setting_name "$config_line" "retrodeck") if [[ ! $current_setting_name =~ (rdhome|sdcard|backups_folder) ]]; then # Ignore these locations @@ -501,8 +507,9 @@ configurator_tools_dialog() { backup_retrodeck_userdata "custom" "${choices[@]}" # Expand array of choices into individual arguments ;; "Backup All Userdata" ) + log i "User chose to backup all userdata prior to update." export CONFIGURATOR_GUI="zenity" - backup_retrodeck_userdata "standard" + backup_retrodeck_userdata "complete" ;; esac