New UI with simple-term-menu, now replicates original Visual Basic program
Finished replicating the original program, Features include: Backup, Restore, and Update.. But there is still more I want to add, such as better settings management, update, and more.
This commit is contained in:
parent
4286ea5ba3
commit
16a4e50f66
4 changed files with 183 additions and 101 deletions
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -1,17 +1,32 @@
|
|||
# Changelog
|
||||
|
||||
## 0.3.1
|
||||
Fixed that online version always was newer. Changed some small layouts from the menu.
|
||||
## 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.3.0
|
||||
Adding an update function. Checks the latest_version.txt file from gitlab and can pull the newest ftl-savemanager.py file and replace the local version.
|
||||
If .gitignore file is found in the same folder update is denied, it also ask if the user wants to update before doing so.
|
||||
### 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
|
||||
- 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.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
|
||||
- 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.1
|
||||
Switching to Semantic versioning, MAJOR.MINOR.PATCH
|
||||
|
||||
## 0.2
|
||||
Reorgenized: The functions have been reorganized into two classes, Backup and UI.
|
||||
- Adopted semantic versioning (MAJOR.MINOR.PATCH) for better version tracking.
|
||||
### 0.2
|
||||
- Reorganized the codebase: functions have been split into two classes, Backup and UI.
|
||||
|
||||
## 0.1
|
||||
Initial working script
|
||||
- Initial working script.
|
||||
|
|
50
README.md
50
README.md
|
@ -1,12 +1,50 @@
|
|||
# FTL Save File Manager
|
||||
|
||||
Recreating the program I wrote in school in VS Basic but in Python.
|
||||
It is a program enabling saving and loading in FTL (Faster Then Light) game.
|
||||
## Description
|
||||
It is a program enabling saving and loading*1 in FTL (Faster Then Light) game.
|
||||
Backup works after a jump, restore only works while in game are in the main menu.
|
||||
|
||||
It is considered as a project to learn and be more confident with Python as well as Git.
|
||||
|
||||
*1 ATM only the current game state, does not include the profile file (ae_prof.sav) which has information about total archiements and unlockes.
|
||||
|
||||
## How it works
|
||||
It has a very simple menu after starting, with option such as backup, restore backup, delete backup files and quite the program.
|
||||
Import to notice how to backup and restore.
|
||||
Backup can be done after a jump, restore does only work when the game is in main menu.
|
||||
## Status
|
||||
As of version 0.4.2 has the program now replicates the functionality of the original Visual Basic I once wrote in high school but rewritten in Python with a terminal-based UI:
|
||||
- Features include: Backup, Restore, and Update.
|
||||
|
||||
# How it works
|
||||
It uses TerminalMenu to show a simple gui in the terminal, you select the save game folder and location to backup saves.
|
||||
Only work on linux and dependencies are requests and simple_terminal_menu
|
||||
|
||||
# FTL Save File Manager
|
||||
## Description
|
||||
FTL Save File Manager is a utility designed to manage save files for the game FTL: Faster Than Light.
|
||||
|
||||
- Backup: Automatically saves your progress after a jump.
|
||||
- Restore: Allows you to restore save files, but only while in the game's main menu.
|
||||
This project also serves as a learning experience to enhance Python programming skills and gain confidence with Git.
|
||||
|
||||
**Note: Currently, only the active game state is managed. The profile file (ae_prof.sav), which contains achievements and unlocks, is not included in backups.**
|
||||
|
||||
# Status
|
||||
As of version 0.4.2, the program fully replicates the functionality of the original Visual Basic program I developed in high school. It has been rewritten in Python and now features a terminal-based UI:
|
||||
|
||||
***Features:***
|
||||
- Backup
|
||||
- Restore
|
||||
- Update
|
||||
|
||||
# How It Works
|
||||
FTL Save File Manager uses simple-term-menu to provide a straightforward terminal-based interface.
|
||||
|
||||
## Steps:
|
||||
1. Select the save game folder.
|
||||
2. Choose a backup location.
|
||||
3. Manage your save files easily through the menu.
|
||||
|
||||
## Requirements
|
||||
Operating System: Linux
|
||||
|
||||
Dependencies:
|
||||
- requests
|
||||
- simple-term-menu
|
||||
|
|
|
@ -1,11 +1,34 @@
|
|||
import os
|
||||
import time
|
||||
import sys
|
||||
import requests
|
||||
from datetime import datetime
|
||||
version = "0.3.1"
|
||||
version = "0.4.2"
|
||||
gitlab_url = "https://gitlab.com/python_projects3802849/ftl-save-manager/-/raw/main"
|
||||
settings_location = "settings.txt"
|
||||
dependencies = ("simple_term_menu", "requests")
|
||||
|
||||
if sys.platform != "linux":
|
||||
print(f"{sys.platform} is not supported, only linux.")
|
||||
exit()
|
||||
|
||||
def check_package_installed(package_name):
|
||||
"""Check if a package is installed."""
|
||||
try:
|
||||
__import__(package_name)
|
||||
return True
|
||||
except ImportError as e:
|
||||
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.
|
||||
from simple_term_menu import TerminalMenu
|
||||
|
||||
class BackupApp:
|
||||
""""Backup class, copy FTL continue file to backup location and restores it."""
|
||||
|
@ -37,11 +60,11 @@ class BackupApp:
|
|||
|
||||
def ask_for_path(self):
|
||||
"""Uses get_path to request path from the user."""
|
||||
game_path = self.get_path("Game save path", True)
|
||||
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):
|
||||
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}: ")
|
||||
|
@ -60,7 +83,7 @@ class BackupApp:
|
|||
continue
|
||||
return path
|
||||
|
||||
def write_settings(self, list):
|
||||
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:
|
||||
|
@ -75,18 +98,6 @@ class BackupApp:
|
|||
os.system(f"cp {src} {dst}")
|
||||
print(f"Wrote: {dst}")
|
||||
|
||||
def list_backup(self):
|
||||
""""List all files inside the backup folder."""
|
||||
files = os.listdir(self.backup_path)
|
||||
files.reverse() # this way the newest file is the top one.
|
||||
self.backup_files.clear()
|
||||
i = 1
|
||||
for file in files:
|
||||
new_item = {f"{i}": file}
|
||||
print(f"[{i}] {file}")
|
||||
self.backup_files.update(new_item)
|
||||
i += 1
|
||||
|
||||
def backup(self):
|
||||
"""Copies the game file to the backup folder and adds date-time stamp."""
|
||||
now = datetime.now()
|
||||
|
@ -94,26 +105,11 @@ class BackupApp:
|
|||
self.copy_file(f"{self.game_path}/continue.sav", f"{self.backup_path}/{formatted_now}.bkup")
|
||||
time.sleep(0.5)
|
||||
|
||||
def restore(self):
|
||||
"""Copies a selected backup file to the game folder and renames it."""
|
||||
print("Select a file to restore (back [b]):")
|
||||
self.list_backup()
|
||||
while True:
|
||||
choice = input("Enter number (b = back): ")
|
||||
if choice in self.backup_files:
|
||||
self.copy_file(f"{self.backup_path}/{self.backup_files[choice]}", f"{self.game_path}/continue.sav")
|
||||
print(f"{self.backup_files[choice]} restored.")
|
||||
time.sleep(0.5)
|
||||
break
|
||||
elif choice == "b":
|
||||
break
|
||||
else:
|
||||
print("not a valid option")
|
||||
continue
|
||||
def restore_backup(self, file):
|
||||
self.copy_file(f"{self.backup_path}/{file}", f"{self.game_path}/continue.sav")
|
||||
|
||||
def delete_backup_file(self):
|
||||
def delete_backup_file(self): # improvment: here also, more generic, delete_file.
|
||||
"""Deltes all .bkup files in the backup folder."""
|
||||
# Making a yes/no function would be cleaner
|
||||
if self.yes_no("Backup"):
|
||||
for file in os.listdir(self.backup_path):
|
||||
if file[-5:] == ".bkup":
|
||||
|
@ -125,7 +121,7 @@ class BackupApp:
|
|||
print(f"Failed to delete {file}. Reason: {e}.")
|
||||
#time.sleep(2)
|
||||
|
||||
def yes_no(self, str):
|
||||
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)")
|
||||
|
@ -138,32 +134,7 @@ class BackupApp:
|
|||
else:
|
||||
print("Not a valid option, try again")
|
||||
|
||||
class SimpleUI:
|
||||
"""'UI for the backup class, simply displayes a menu and accepts letter to choice option."""
|
||||
def __init__(self, options, names):
|
||||
self.options = options
|
||||
self.names = names
|
||||
|
||||
def menu(self):
|
||||
""""Use dict to select an option (function)."""
|
||||
self.print_menu()
|
||||
while True:
|
||||
choice = input("Select Option: ")
|
||||
print("\n")
|
||||
if choice in self.options:
|
||||
self.options[choice]()
|
||||
self.print_menu()
|
||||
else:
|
||||
print("Try again")
|
||||
continue
|
||||
|
||||
def print_menu(self):
|
||||
#print("\033[H\033[J", end="")
|
||||
print("Menu options")
|
||||
for key in self.options:
|
||||
print(f"{key}: {self.names[key]}")
|
||||
|
||||
class Updater:
|
||||
class Updater: # improvment: if dependency not aviable disable this
|
||||
"""Update local version from gitlab repo."""
|
||||
def __init__(self, version, update_url, local_file):
|
||||
self.current_version = version
|
||||
|
@ -180,10 +151,11 @@ class Updater:
|
|||
latest_version = response.text.strip()
|
||||
|
||||
if self.compare_SemVer(latest_version):
|
||||
print(f"New version {latest_version} available.\nCurrent version: {self.current_version}")
|
||||
print(f"New version {latest_version} available online.\nCurrent version: {self.current_version}")
|
||||
return True
|
||||
else:
|
||||
print("You are already on the latest version.")
|
||||
print(f"You are up-to-date, the latest version is: {latest_version}.")
|
||||
time.sleep(2)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error checking for updates: {e}")
|
||||
|
@ -199,7 +171,7 @@ class Updater:
|
|||
"""Converts a list with strings to a list with intengers."""
|
||||
return [int(i) for i in list_str]
|
||||
|
||||
def download_update(self, latest_version):
|
||||
def download_update(self, latest_version): # 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}...")
|
||||
|
@ -222,6 +194,7 @@ class Updater:
|
|||
"""Main method to check, update, and restart."""
|
||||
if self.git:
|
||||
print("Updating only works outside git env.")
|
||||
time.sleep(2)
|
||||
else:
|
||||
latest_version = self.check_for_update()
|
||||
if latest_version:
|
||||
|
@ -233,7 +206,7 @@ class Updater:
|
|||
print("Update cancelled")
|
||||
return
|
||||
|
||||
def yes_no(self, str):
|
||||
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)")
|
||||
|
@ -244,27 +217,83 @@ class Updater:
|
|||
else:
|
||||
print("Not a valid option, try again")
|
||||
|
||||
class TerminalUI:
|
||||
def __init__(self, title, options):
|
||||
self.main_menu_title = title
|
||||
self.main_menu_options = options
|
||||
self.main_menu_keys = list(self.main_menu_options.keys()) # terminal ui takes a list for the menu
|
||||
self.main_menu = TerminalMenu(
|
||||
menu_entries = self.main_menu_keys,
|
||||
title = self.main_menu_title,
|
||||
menu_cursor = "> ",
|
||||
menu_cursor_style = ("fg_gray", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = 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]()
|
||||
|
||||
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",
|
||||
menu_cursor = "> ",
|
||||
menu_cursor_style = ("fg_red", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = True)
|
||||
|
||||
menu_entry_index = folder_menu.show()
|
||||
if menu_entry_index == None:
|
||||
return
|
||||
else:
|
||||
app.restore_backup(menu_options[menu_entry_index])
|
||||
|
||||
def list_files(self, directory):
|
||||
return (file for file in sorted(os.listdir(directory), reverse = True) if os.path.isfile(os.path.join(directory, file)) and file.endswith((".bkup")))
|
||||
|
||||
def yes_no_menu(self, message): # oh
|
||||
menu_options = ["[y] yes", "[n] no"]
|
||||
menu = TerminalMenu(menu_entries = menu_options,
|
||||
title = f"{message}",
|
||||
menu_cursor = "> ",
|
||||
menu_cursor_style = ("fg_red", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = True)
|
||||
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)
|
||||
|
||||
# Designing the menu options from the backupapp and the layout
|
||||
menu_options = {
|
||||
"b": app.backup,
|
||||
"r": app.restore,
|
||||
"d": app.delete_backup_file,
|
||||
"u": up.run_update,
|
||||
"q": exit
|
||||
}
|
||||
menu_names = {
|
||||
"b": "Backup now",
|
||||
"r": "Restore backup",
|
||||
"d": "Delete backup files",
|
||||
"u": "Update",
|
||||
"q": "Exit"
|
||||
|
||||
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,
|
||||
"[q] Quit": exit
|
||||
}
|
||||
|
||||
# Loading the ui and the options
|
||||
sui = SimpleUI(menu_options, menu_names)
|
||||
title = f"ftl-savemanager v{version} (Press Q or Esc to quit).\n"
|
||||
tui = TerminalUI(title, main_menu_options)
|
||||
|
||||
# starting the ui i.e. program
|
||||
sui.menu()
|
||||
|
||||
|
||||
tui.show_main_menu()
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.3.1
|
||||
0.4.2
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue