Now included all wanted function. Auto update, change settings, change repo and more.
New Class for function used across all other classes. Automatically check for new updates. Change repo url, handy for development. Backup profile file, change some settings in the program and more. See changelog for greater detail.
This commit is contained in:
parent
65e6f5a4d7
commit
98731a534d
4 changed files with 391 additions and 150 deletions
52
CHANGELOG.md
52
CHANGELOG.md
|
@ -1,32 +1,74 @@
|
|||
# Changelog
|
||||
|
||||
# 0.5.x
|
||||
## 0.5.3
|
||||
- Merged to stable; all desired functions are now included.
|
||||
- Next steps: optimizing classes and functions to reduce dependencies and improve modularity.
|
||||
|
||||
## 0.5.2
|
||||
### FIXES
|
||||
- Fixed an issue where loading i.e. restore save, did not work.
|
||||
|
||||
### New
|
||||
- **Utility class**
|
||||
- Introduced a new Utility class to organize functions and improve code cleanliness. More functions may be moved in future updates.
|
||||
|
||||
- **Two new menus**
|
||||
- Added a settings menu, allowing (some) settings to be changed directly within the program.
|
||||
- Added a menu to backup and restore FTL profile files. It now checks if the destination is newer to prevent overwriting progress by restoring an outdated profile.
|
||||
### Improvments
|
||||
- Improved flexibility of some functions.
|
||||
|
||||
## 0.5.1
|
||||
### Improvments
|
||||
- Settings Enhancements:
|
||||
- Extended settings to include LAST_ONLINE_VERSION, LAST_UPDATE_CHECK, and AUTO_UPDATE_CHECK_INTERVAL_H.
|
||||
Added support for default values to be automatically added to the settings file if missing, simplifying updates with new settings.
|
||||
|
||||
### New functions
|
||||
- Auto Update Check:
|
||||
- Introduced automatic update checks, defaulting to every 72 hours (configurable via the settings file).
|
||||
- The latest online version number is now saved in the local settings file.
|
||||
- Update Notification:
|
||||
- When a new version is available online, the latest version number is displayed next to the update button.
|
||||
### Planned Enhancements:
|
||||
- Add a function allowing users to configure all settings manually.
|
||||
|
||||
## 0.5.0
|
||||
- Refactored functions related to loading and writing settings into a dedicated class, enhancing flexibility and maintainability for managing settings files.
|
||||
- Improved settings file handling, enabling more robust and adaptable loading and saving.
|
||||
- Updated the README to include details on how LLMs were utilized during development.
|
||||
|
||||
# 0.4.x
|
||||
## 0.4.2
|
||||
- Cleaning: Removed old, unused code.
|
||||
- The program now replicates the functionality of the original Visual Basic program but rewritten in Python with a terminal-based UI:
|
||||
- Features include: Backup, Restore, and Update.
|
||||
|
||||
### 0.4.1
|
||||
## 0.4.1
|
||||
- Added a check to ensure the system is Linux and all required dependencies are installed; exits gracefully if not.
|
||||
- Removed the old UI, as maintaining two versions is unnecessary.
|
||||
- Updated file selection to use simple_term_menu for improved functionality.
|
||||
|
||||
### 0.4.0
|
||||
## 0.4.0
|
||||
- Introduced a new terminal UI using simple_term_menu for the main menu.
|
||||
- This adds a new dependency (simple_term_menu) that must be installed for the UI to function.
|
||||
- The program checks if the package is available. If it is, the new UI is loaded; otherwise, it falls back to the simpler version.
|
||||
|
||||
# 0.3.x
|
||||
## 0.3.1
|
||||
- Fixed an issue where the online version was always considered newer, regardless of the actual version numbers.
|
||||
- Improved menu layout with minor changes for better usability.
|
||||
### 0.3.0
|
||||
## 0.3.0
|
||||
- Added an update function. The program now checks the latest_version.txt file from GitLab and can download and replace the local ftl-savemanager.py file with the latest version.
|
||||
- If a .gitignore file is detected in the same folder, the update process is blocked.
|
||||
- The program prompts the user for confirmation before performing the update.
|
||||
|
||||
# 0.2.x
|
||||
## 0.2.1
|
||||
- Adopted semantic versioning (MAJOR.MINOR.PATCH) for better version tracking.
|
||||
### 0.2
|
||||
## 0.2
|
||||
- Reorganized the codebase: functions have been split into two classes, Backup and UI.
|
||||
|
||||
## 0.1
|
||||
# 0.1
|
||||
- Initial working script.
|
||||
|
|
|
@ -48,3 +48,9 @@ Operating System: Linux
|
|||
Dependencies:
|
||||
- requests
|
||||
- simple-term-menu
|
||||
|
||||
Can be installed with pip or using miniconda (aka conda/mamba)
|
||||
|
||||
# Additional Note
|
||||
This project was developed with the assistance of LLMs (including, but not limited to, OpenAI's ChatGPT, Ollama models like OpenCoder and LLaMA 3.2).
|
||||
The assistance provided includes, but is not limited to, spelling corrections, function suggestions, and discussions of debugging, which are not explicitly labeled. Direct optimizations of the code are noted within the code itself.
|
||||
|
|
|
@ -2,7 +2,8 @@ import os
|
|||
import time
|
||||
import sys
|
||||
from datetime import datetime
|
||||
version = "0.4.2"
|
||||
|
||||
version = "0.5.2" # actully 0.5.2 because 0.5.1 was just pushed
|
||||
gitlab_url = "https://gitlab.com/python_projects3802849/ftl-save-manager/-/raw/main"
|
||||
settings_location = "settings.txt"
|
||||
dependencies = ("simple_term_menu", "requests")
|
||||
|
@ -20,161 +21,128 @@ def check_package_installed(package_name):
|
|||
print(f"{package_name} not found. Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
for item in dependencies:
|
||||
if check_package_installed(item) == False:
|
||||
print(f"One or more dependencies are missing, please ensure follwing packaged are installed:\nrequests\nsimple-term-menu")
|
||||
exit()
|
||||
|
||||
|
||||
import requests # todo: add check if installed, if not disable update function.
|
||||
import requests
|
||||
from simple_term_menu import TerminalMenu
|
||||
|
||||
class BackupApp:
|
||||
""""Backup class, copy FTL continue file to backup location and restores it."""
|
||||
def __init__(self, settings_file_path):
|
||||
def __init__(self):
|
||||
self.backup_files = {"1": "initiating"} # initiating variable
|
||||
self.settings_path = settings_file_path
|
||||
self.settings = self.read_settings()
|
||||
|
||||
if self.settings:
|
||||
self.game_path = self.settings[0]
|
||||
self.backup_path = self.settings[1]
|
||||
else:
|
||||
self.game_path, self.backup_path = self.ask_for_path()
|
||||
self.write_settings([self.game_path, self.backup_path])
|
||||
|
||||
print(f"Game location: {self.game_path}.\nSave location: {self.backup_path}")
|
||||
|
||||
def read_settings(self):
|
||||
"""Reading the settings file."""
|
||||
if os.path.exists(self.settings_path):
|
||||
with open(self.settings_path, "r") as f:
|
||||
data = f.readlines()
|
||||
for i in range(len(data)):
|
||||
data[i] = data[i].strip() #stripping white space
|
||||
return(data)
|
||||
else:
|
||||
print("settings file does not exit.")
|
||||
return False
|
||||
|
||||
def ask_for_path(self):
|
||||
"""Uses get_path to request path from the user."""
|
||||
game_path = self.get_path("Game save path", True) #
|
||||
backup_path = self.get_path("Backup path")
|
||||
return(game_path, backup_path)
|
||||
|
||||
def get_path(self, str, ftl = False): # improvment: function that checks path or file
|
||||
"""Get path from user and check if exists."""
|
||||
while True:
|
||||
path = input(f"please enter {str}: ")
|
||||
if ftl:
|
||||
profile_file = path + "/ae_prof.sav"
|
||||
if os.path.isfile(profile_file):
|
||||
return path
|
||||
else:
|
||||
print("Path is not valid, try again.")
|
||||
continue
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
break
|
||||
else:
|
||||
print("Path not valid, try again.")
|
||||
continue
|
||||
return path
|
||||
|
||||
def write_settings(self, list): # improvment: more generic name, write_file or write_list_as_file
|
||||
"""Write the settings (path)"""
|
||||
with open(self.settings_path, "w") as f:
|
||||
for item in list:
|
||||
f.write(item + "\n")
|
||||
print("write settings.")
|
||||
|
||||
def copy_file(self, src, dst):
|
||||
""""Simply copies a file from a to b."""
|
||||
if not os.path.isfile(src):
|
||||
print(f"File to copy {src} does not exist.")
|
||||
return False
|
||||
os.system(f"cp {src} {dst}")
|
||||
print(f"Wrote: {dst}")
|
||||
self.game_path = configuration.settings["GAME_PATH"]
|
||||
self.save_path = configuration.settings["SAVE_PATH"]
|
||||
|
||||
def backup(self):
|
||||
"""Copies the game file to the backup folder and adds date-time stamp."""
|
||||
now = datetime.now()
|
||||
formatted_now = now.strftime("%Y-%m-%d_%H-%M")
|
||||
self.copy_file(f"{self.game_path}/continue.sav", f"{self.backup_path}/{formatted_now}.bkup")
|
||||
formatted_now = now.strftime("%Y-%m-%d_%H-%M-%S")
|
||||
u.copy_file(f"{self.game_path}/continue.sav", f"{self.save_path}/{formatted_now}.bkup")
|
||||
time.sleep(0.5)
|
||||
|
||||
def restore_backup(self, file):
|
||||
self.copy_file(f"{self.backup_path}/{file}", f"{self.game_path}/continue.sav")
|
||||
u.copy_file(f"{self.save_path}/{file}", f"{self.game_path}/continue.sav")
|
||||
|
||||
def delete_backup_file(self): # improvment: here also, more generic, delete_file.
|
||||
def delete_backup_file(self): # improvment: here also, more generic, delete_files with file type .something
|
||||
"""Deltes all .bkup files in the backup folder."""
|
||||
if self.yes_no("Backup"):
|
||||
for file in os.listdir(self.backup_path):
|
||||
if u.yes_no("Are you sure you want to delete all save files?"):
|
||||
for file in os.listdir(self.save_path):
|
||||
if file[-5:] == ".bkup":
|
||||
try:
|
||||
print(f"Deleting {file}.")
|
||||
os.remove(f"{self.backup_path}/{file}")
|
||||
os.remove(f"{self.save_path}/{file}")
|
||||
time.sleep(0.3)
|
||||
except Exception as e:
|
||||
print(f"Failed to delete {file}. Reason: {e}.")
|
||||
#time.sleep(2)
|
||||
time.sleep(1)
|
||||
else:
|
||||
print("Deleting save files canceled")
|
||||
time.sleep(0.5)
|
||||
|
||||
def yes_no(self, str): # improvment: more generic, take two string.
|
||||
"""Ask user y/n question regaring deletion of a string."""
|
||||
while True:
|
||||
choice = input(f"Are you sure you want to delete all {str} files? (y/n)")
|
||||
if choice == "y":
|
||||
return True
|
||||
elif choice == "n":
|
||||
print(f"Deleting {str} files aborted")
|
||||
#time.sleep(1)
|
||||
return False
|
||||
else:
|
||||
print("Not a valid option, try again")
|
||||
|
||||
class Updater: # improvment: if dependency not aviable disable this
|
||||
class Updater:
|
||||
"""Update local version from gitlab repo."""
|
||||
def __init__(self, version, update_url, local_file):
|
||||
def __init__(self, version, local_file):
|
||||
self.current_version = version
|
||||
self.update_url = update_url
|
||||
self.update_url = configuration.settings["REPO_URL"]
|
||||
self.local_file = local_file
|
||||
self.git = os.path.exists(".gitignore") # Checking if the program inside git env
|
||||
self.auto_check()
|
||||
|
||||
def check_for_update(self):
|
||||
def auto_check(self):
|
||||
print("Auto update check")
|
||||
check_interval = configuration.settings["AUTO_UPDATE_CHECK_INTERVAL_H"]
|
||||
if check_interval == 0:
|
||||
print("Auto update disabeld")
|
||||
return # Exiting
|
||||
if "LAST_UPDATE_CHECK" in configuration.settings:
|
||||
last_time = configuration.settings["LAST_UPDATE_CHECK"]
|
||||
t_object = datetime.strptime(last_time, "%Y-%m-%d %H-%M")
|
||||
try:
|
||||
h_since_update_check = u.get_difference_in_hours(datetime.now(), t_object)
|
||||
if h_since_update_check > int(configuration.settings["AUTO_UPDATE_CHECK_INTERVAL_H"]):
|
||||
print(f"More then {configuration.settings["AUTO_UPDATE_CHECK_INTERVAL_H"]} hours since last update check, checking now...")
|
||||
self.check_for_update(0)
|
||||
except ValueError:
|
||||
print("Invalid timestamp format for LAST_UPDATE_CHECK.")
|
||||
input("Error checking for updates:\nEnter to continue...")
|
||||
else:
|
||||
self.check_for_update(0)
|
||||
|
||||
def check_for_update(self, verbose = 1):
|
||||
"""Check if a new version is available."""
|
||||
try:
|
||||
print("Checking for updates...")
|
||||
if verbose: print("Checking for updates...")
|
||||
response = requests.get(f"{self.update_url}/latest_version.txt")
|
||||
response.raise_for_status()
|
||||
latest_version = response.text.strip()
|
||||
|
||||
if self.compare_SemVer(latest_version):
|
||||
print(f"New version {latest_version} available online.\nCurrent version: {self.current_version}")
|
||||
self.update_info = self.read_version_file(response)
|
||||
configuration.settings["LAST_ONLINE_VERSION"] = self.update_info["LATEST_VERSION"] # updating the version number from update class to global var
|
||||
now = datetime.now()
|
||||
formatted_now = now.strftime("%Y-%m-%d %H-%M")
|
||||
configuration.settings["LAST_UPDATE_CHECK"] = formatted_now
|
||||
if self.compare_str_SemVer(self.current_version, self.update_info["LATEST_VERSION"]):
|
||||
if verbose:
|
||||
print(f"New version {self.update_info["LATEST_VERSION"]} available online.\nCurrent version: {self.current_version}")
|
||||
print(f"\nUpdate comment:\n{self.update_info["COMMENT"]}\n")
|
||||
return True
|
||||
else:
|
||||
print(f"You are up-to-date, the latest version is: {latest_version}.")
|
||||
time.sleep(2)
|
||||
if verbose:
|
||||
print(f"You are up-to-date ({self.current_version}).\nLatest version is: {self.update_info["LATEST_VERSION"]}.")
|
||||
input("Enter to continue...")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error checking for updates: {e}")
|
||||
if verbose: print(f"Error checking for updates: {e}")
|
||||
input("UPDATE ERROR Press Enter to continue...")
|
||||
return False
|
||||
|
||||
def compare_SemVer(self, latest_version):
|
||||
def read_version_file(self, response): # does basicly the same as the read settings function in settingmanger class, optimizing would combine them.
|
||||
settings = {}
|
||||
lines = response.text.splitlines()
|
||||
for line in lines:
|
||||
if line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = map(str.strip, line.split("=", 1))
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
settings[key] = value[1:-1]
|
||||
return settings
|
||||
|
||||
def compare_str_SemVer(self, first, second):
|
||||
"""Compare two Semantic versioning strings."""
|
||||
local_version = self.convert_str_list_to_int(self.current_version.split("."))
|
||||
online_version = self.convert_str_list_to_int(latest_version.split("."))
|
||||
local_version = self.convert_str_list_to_int(first.split("."))
|
||||
online_version = self.convert_str_list_to_int(second.split("."))
|
||||
return local_version < online_version
|
||||
|
||||
def convert_str_list_to_int(self, list_str):
|
||||
"""Converts a list with strings to a list with intengers."""
|
||||
return [int(i) for i in list_str]
|
||||
|
||||
def download_update(self, latest_version): # improvment: silly it takes the latest_version as variable
|
||||
def download_update(self): # improvment: silly it takes the latest_version as variable
|
||||
"""Download the new version and overwrite the current file."""
|
||||
try:
|
||||
print(f"Downloading version {latest_version}...")
|
||||
print(f"Downloading version {self.update_info["LATEST_VERSION"]}...")
|
||||
time.sleep(2)
|
||||
response = requests.get(f"{self.update_url}/{self.local_file}")
|
||||
response.raise_for_status()
|
||||
with open(self.local_file, "wb") as file:
|
||||
|
@ -196,27 +164,15 @@ class Updater: # improvment: if dependency not aviable disable this
|
|||
print("Updating only works outside git env.")
|
||||
time.sleep(2)
|
||||
else:
|
||||
latest_version = self.check_for_update()
|
||||
if latest_version:
|
||||
if self.yes_no("Do you want to update the program?"):
|
||||
success = self.download_update(latest_version)
|
||||
if self.check_for_update():
|
||||
if u.yes_no("Do you want to update the program?"):
|
||||
success = self.download_update()
|
||||
if success:
|
||||
self.restart_program()
|
||||
else:
|
||||
print("Update cancelled")
|
||||
return
|
||||
|
||||
def yes_no(self, str): # improvment: have two yes_no function, should merge them
|
||||
"""Ask user y/n question"""
|
||||
while True:
|
||||
choice = input(f"{str} (y/n)")
|
||||
if choice == "y":
|
||||
return True
|
||||
elif choice == "n":
|
||||
return False
|
||||
else:
|
||||
print("Not a valid option, try again")
|
||||
|
||||
class TerminalUI:
|
||||
def __init__(self, title, options):
|
||||
self.main_menu_title = title
|
||||
|
@ -229,31 +185,40 @@ class TerminalUI:
|
|||
menu_cursor_style = ("fg_gray", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = True,
|
||||
clear_screen = True
|
||||
)
|
||||
|
||||
def show_main_menu(self):
|
||||
while True:
|
||||
selected_index = self.main_menu.show()
|
||||
selected_key = None
|
||||
if type(selected_index) == int: selected_key = self.main_menu_keys[selected_index]
|
||||
|
||||
if selected_index == None:
|
||||
break
|
||||
elif selected_key == "[r] Restore backup":
|
||||
self.restore_file_menu(app.backup_path)
|
||||
elif selected_key in self.main_menu_options:
|
||||
self.main_menu_options[selected_key]()
|
||||
# Adding try makes it possiable to add code after the True loop
|
||||
try:
|
||||
while True:
|
||||
selected_index = self.main_menu.show()
|
||||
selected_key = None
|
||||
if isinstance(selected_index, int):
|
||||
selected_key = self.main_menu_keys[selected_index]
|
||||
if selected_index == None:
|
||||
break
|
||||
elif selected_key == "[l] Load":
|
||||
self.restore_file_menu(app.save_path)
|
||||
elif selected_key == "[c] Change settings":
|
||||
self.change_settings_menu()
|
||||
elif selected_key == "[p] Profile managment":
|
||||
self.profile_menu()
|
||||
elif selected_key in self.main_menu_options:
|
||||
self.main_menu_options[selected_key]()
|
||||
finally:
|
||||
configuration.write_settings_file() # writing settings when exiting.
|
||||
|
||||
def restore_file_menu(self, folder):
|
||||
menu_options = list(self.list_files(folder))
|
||||
folder_menu = TerminalMenu(menu_entries = menu_options,
|
||||
title = f"Content of {folder} (Q or Esc to go back.)\n",
|
||||
title = f"Content of {folder} (Q or Esc to go back).\n",
|
||||
menu_cursor = "> ",
|
||||
menu_cursor_style = ("fg_red", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = True)
|
||||
clear_screen = False,
|
||||
)
|
||||
|
||||
menu_entry_index = folder_menu.show()
|
||||
if menu_entry_index == None:
|
||||
|
@ -272,28 +237,255 @@ class TerminalUI:
|
|||
menu_cursor_style = ("fg_red", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = True)
|
||||
clear_screen = False)
|
||||
menu_entry_index = menu.show()
|
||||
if menu_entry_index == 0:
|
||||
return True
|
||||
elif menu_entry_index == 1:
|
||||
return False
|
||||
|
||||
up = Updater(version, gitlab_url, "ftl-savemanager.py")
|
||||
app = BackupApp(settings_location)
|
||||
def change_settings_menu(self):
|
||||
menu_options = [f"[g] Game path ({configuration.settings["GAME_PATH"]})",
|
||||
f"[s] Save path ({configuration.settings["SAVE_PATH"]})",
|
||||
f"[a] Auto update interval ({configuration.settings["AUTO_UPDATE_CHECK_INTERVAL_H"]})",
|
||||
"[q] Quite"]
|
||||
settings_option = ["GAME_PATH", "SAVE_PATH", "AUTO_UPDATE_CHECK_INTERVAL_H"]
|
||||
settings_menu = TerminalMenu(menu_entries = menu_options,
|
||||
title = "Change settings (Q or Esc to go back).\n",
|
||||
menu_cursor = "> ",
|
||||
menu_cursor_style = ("fg_red", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = False
|
||||
)
|
||||
try:
|
||||
while True:
|
||||
selected_index = settings_menu.show()
|
||||
selected_key = None
|
||||
if selected_index == 3:
|
||||
break
|
||||
if isinstance(selected_index, int):
|
||||
selected_key = settings_option[selected_index]
|
||||
if selected_index == None:
|
||||
break
|
||||
elif selected_key in settings_option:
|
||||
if self.yes_no_menu(f"Do you want to change {selected_key} from {configuration.settings[selected_key]}?"):
|
||||
new_value = input(f"Please enter new value for {selected_key}: ")
|
||||
configuration.settings[selected_key] = new_value
|
||||
configuration.validate_paths_in_settings()
|
||||
configuration.convert_str_to_int_settings()
|
||||
else:
|
||||
print("No changed were made.")
|
||||
finally:
|
||||
configuration.write_settings_file() # writing settings when exiting.
|
||||
|
||||
def profile_menu(self):
|
||||
menu_options = ["[b] Backup profile", "[r] Restore profile", "[q] Quite"]
|
||||
menu = TerminalMenu(menu_entries = menu_options,
|
||||
title = f"Backup or restore the profile file, which includes complete progress (Q or Esc to go back).\n",
|
||||
menu_cursor = "> ",
|
||||
menu_cursor_style = ("fg_red", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = False)
|
||||
menu_entry_index = menu.show()
|
||||
profile_path = f"{configuration.settings["GAME_PATH"]}/ae_prof.sav"
|
||||
backup_path = f"{configuration.settings["SAVE_PATH"]}/ae_prof.sav"
|
||||
|
||||
if menu_entry_index == 0:
|
||||
u.copy_file_overwrite_if_newer(profile_path, backup_path)
|
||||
elif menu_entry_index == 1:
|
||||
u.copy_file_overwrite_if_newer(backup_path, profile_path)
|
||||
else:
|
||||
return
|
||||
|
||||
class SettingManager:
|
||||
"""Handles program settings, reading, validating, and writing them."""
|
||||
# Refactored this class with suggestions from ChatGPT for better readability (code and language) and efficiency
|
||||
def __init__(self, settings_path, repo_url):
|
||||
self.accepted_settings_keys = ["GAME_PATH", "SAVE_PATH", "LAST_ONLINE_VERSION", "REPO_URL", "LAST_UPDATE_CHECK", "AUTO_UPDATE_CHECK_INTERVAL_H"]
|
||||
self.settings_file_path = settings_path
|
||||
self.settings = self.read_settings_file()
|
||||
# Initialize settings
|
||||
if self.settings:
|
||||
self.validate_paths_in_settings()
|
||||
self.set_default_values(repo_url)
|
||||
else:
|
||||
self.initialize_settings(repo_url)
|
||||
self.convert_str_to_int_settings()
|
||||
|
||||
def set_default_values(self, repo_url):
|
||||
default_values = {"REPO_URL": repo_url,
|
||||
"AUTO_UPDATE_CHECK_INTERVAL_H": "72"
|
||||
}
|
||||
for key in default_values:
|
||||
if key not in self.settings:
|
||||
self.settings[key] = default_values[key]
|
||||
print(f"Setting default value: {default_values[key]} for: {key}.")
|
||||
#time.sleep(0.5)
|
||||
|
||||
def convert_str_to_int_settings(self):
|
||||
"""Change str to int for a given keys in the settings, if not convertable then replace with default value"""
|
||||
int_values = {"AUTO_UPDATE_CHECK_INTERVAL_H": 72} # Dict for all variables which sould be intinger, and their default values. Scable for future
|
||||
for key in int_values:
|
||||
try:
|
||||
self.settings[key] = int(self.settings[key])
|
||||
except ValueError:
|
||||
print(f"Value: {self.settings[key]} could not be changed to an int.")
|
||||
self.settings[key] = int_values[key]
|
||||
print(f"Changing {key} to default value value: {int_values[key]}\nAcknowledged...")
|
||||
time.sleep(1)
|
||||
|
||||
def initialize_settings(self, repo_url): # URL in settings file has higher priority, is good for feature branches.
|
||||
"""Initialize settings when the file doesn't exist or is empty."""
|
||||
print("Initializing settings...")
|
||||
self.set_default_values(repo_url)
|
||||
self.get_paths()
|
||||
self.write_settings_file()
|
||||
|
||||
def validate_paths_in_settings(self):
|
||||
"""Validate and set paths from the settings file."""
|
||||
required_paths = ["GAME_PATH", "SAVE_PATH"]
|
||||
if all(self.settings.get(path) for path in required_paths):
|
||||
valid_game_path = u.check_path_existence(self.settings["GAME_PATH"], "ae_prof.sav")
|
||||
valid_save_path = u.check_path_existence(self.settings["SAVE_PATH"])
|
||||
if valid_game_path and valid_save_path:
|
||||
print("Paths validated successfully.")
|
||||
return
|
||||
print("Invalid paths detected. Requesting user input.")
|
||||
self.get_paths()
|
||||
self.write_settings_file()
|
||||
|
||||
def read_settings_file(self):
|
||||
"""Read settings from the file."""
|
||||
settings = {}
|
||||
if os.path.exists(self.settings_file_path):
|
||||
try:
|
||||
with open(self.settings_file_path, "r") as file:
|
||||
for line in file:
|
||||
if line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = map(str.strip, line.split("=", 1))
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
settings[key] = value[1:-1]
|
||||
return settings
|
||||
except Exception as e:
|
||||
print(f"Error reading settings file: {e}")
|
||||
print("Settings file does not exist or is invalid.")
|
||||
return settings
|
||||
|
||||
def write_settings_file(self):
|
||||
"""Write the settings dictionary to the file."""
|
||||
print("Writing settings")
|
||||
try:
|
||||
with open(self.settings_file_path, "w") as file:
|
||||
file.write("# This file is rewritten before program closes. Comments are ignored.\n# Setting AUTO_UPDATE_CHECK_INTERVAL_H = '0' disables automatic update checks.\n# DO NOT modify REPO_URL if you don't know what value to set, no function to fix incorrect input.\n")
|
||||
for key in self.accepted_settings_keys:
|
||||
if key in self.settings:
|
||||
value = self.settings[key]
|
||||
file.write(f'{key} = "{value}"\n')
|
||||
except Exception as e:
|
||||
input(f"Error writing settings file: {e}\nEnter to continue")
|
||||
|
||||
def get_paths(self):
|
||||
"""Request and validate paths from the user."""
|
||||
self.settings["GAME_PATH"] = self.ask_for_path("Game path", "ae_prof.sav")
|
||||
self.settings["SAVE_PATH"] = self.ask_for_path("Save path")
|
||||
|
||||
def ask_for_path(self, description, required_file = None):
|
||||
"""Prompt the user for a valid path."""
|
||||
while True:
|
||||
path = input(f"Please enter the {description}: ")
|
||||
if u.check_path_existence(path, required_file):
|
||||
return path
|
||||
print("Invalid path. Please try again.")
|
||||
|
||||
class Utility():
|
||||
@staticmethod
|
||||
def check_path_existence(path, required_file = None):
|
||||
"""Check if a path exists and optionally if it contains a specific file."""
|
||||
if required_file:
|
||||
full_path = os.path.join(path, required_file)
|
||||
return os.path.isfile(full_path)
|
||||
return os.path.isdir(path)
|
||||
|
||||
@staticmethod
|
||||
def get_difference_in_hours(dt1, dt2):
|
||||
diff = abs(dt1 - dt2)
|
||||
hours_difference = int(diff.total_seconds() / 3600)
|
||||
return hours_difference
|
||||
|
||||
@staticmethod
|
||||
def yes_no(str): # improvment: have two yes_no function, should merge them
|
||||
"""Ask user y/n question"""
|
||||
while True:
|
||||
choice = input(f"{str} (y/n): ")
|
||||
if choice == "y":
|
||||
return True
|
||||
elif choice == "n":
|
||||
return False
|
||||
else:
|
||||
print(f"Not a valid option, try again.")
|
||||
|
||||
@staticmethod
|
||||
def copy_file(src, dst):
|
||||
""""Simply copies a file from a to b."""
|
||||
if not os.path.isfile(src):
|
||||
print(f"File to copy {src} does not exist.")
|
||||
return False
|
||||
os.system(f"cp {src} {dst}")
|
||||
print(f"Wrote: {dst}")
|
||||
|
||||
@staticmethod
|
||||
def copy_file_overwrite_if_newer(source, destination):
|
||||
# Main part arrived from opencoder ollama, modified to fit my needs
|
||||
# check if source file exists
|
||||
if not os.path.exists(source):
|
||||
print(f"Source file {source} does not exist.")
|
||||
return False
|
||||
|
||||
# get modification times of source and destination files
|
||||
src_mtime = os.path.getmtime(source)
|
||||
dest_mtime = 0
|
||||
if os.path.exists(destination):
|
||||
dest_mtime = os.path.getmtime(destination)
|
||||
|
||||
if src_mtime < dest_mtime:
|
||||
print(f"Destination file is newer than the source.\n({destination} vs {source})")
|
||||
# ask user permission to overwrite
|
||||
if u.yes_no("Do you want to replace?"):
|
||||
os.system(f"cp {source} {destination}")
|
||||
print(f"File {source} has been copied to {destination}")
|
||||
else:
|
||||
print(f"User chose not to replace {destination}.")
|
||||
else:
|
||||
print(f"Source file is older than or equal to the destination.\n ({source} vs {destination})")
|
||||
os.system(f"cp {source} {destination}")
|
||||
print(f"File {source} has been copied to {destination}")
|
||||
time.sleep(1)
|
||||
return True
|
||||
|
||||
u = Utility()
|
||||
configuration = SettingManager(settings_location, gitlab_url)
|
||||
up = Updater(version, "ftl-savemanager.py")
|
||||
app = BackupApp()
|
||||
|
||||
update_key = "[u] Update"
|
||||
if "LAST_ONLINE_VERSION" in configuration.settings:
|
||||
if up.compare_str_SemVer(version, configuration.settings["LAST_ONLINE_VERSION"]):
|
||||
update_key += f" (v{configuration.settings["LAST_ONLINE_VERSION"]})"
|
||||
|
||||
main_menu_options = {
|
||||
"[b] Backup now": app.backup,
|
||||
"[r] Restore backup": "restore_file_menu",
|
||||
"[d] Delete backup files": app.delete_backup_file,
|
||||
"[u] Look for update": up.run_update,
|
||||
"[s] Save": app.backup,
|
||||
"[l] Load": "restore_file_menu",
|
||||
"[d] Delete save files": app.delete_backup_file,
|
||||
f"{update_key}": up.run_update,
|
||||
"[p] Profile managment": "backup and restore profile",
|
||||
"[c] Change settings": "change:_settings_menu",
|
||||
"[q] Quit": exit
|
||||
}
|
||||
|
||||
title = f"ftl-savemanager v{version} (Press Q or Esc to quit).\n"
|
||||
tui = TerminalUI(title, main_menu_options)
|
||||
|
||||
|
||||
|
||||
tui.show_main_menu()
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.4.2
|
||||
LATEST_VERSION="0.5.3"
|
||||
COMMENT="With this version,the program should contain all needed functions."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue