diff --git a/.gitignore b/.gitignore index 6db02d8..e95c9cd 100644 --- a/.gitignore +++ b/.gitignore @@ -162,5 +162,8 @@ cython_debug/ # Other *.ini *.conf -dist/ -.vscode \ No newline at end of file +.vscode +test/ +steamcmd +t7xwd_library.json +*.exe \ No newline at end of file diff --git a/README.md b/README.md index 8004254..076fc5c 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,45 @@ -# T7x Workshop Downloader (T7xWD) -- A Feature-rich GUI Steam Workshop downloader for BO3 ([T7x client](https://github.com/Ezz-lol/T7x-free)) built using CustomTkinter
+# T7xWD +- A Feature-rich GUI Steam Workshop downloader for [Call of Duty®: Black Ops III](https://store.steampowered.com/app/311210/Call_of_Duty_Black_Ops_III/) built using CustomTkinter
-
- -
- - -
- -
- - -
-
+ + + + + + +
+ + + + + +
-## Usage (exe): -- Run [T7xWD.exe](https://git.rimmyscorner.com/Rim/T7x-Workshop-Downloader/releases/download/latest/T7xWD.exe) and use it (it'll ask you to download steamcmd within the app if not found) -- That's it slap in your workshop item link or just the id then hit Download and wait for it to finish, when it does just launch your game (Please check [Notes](#notes) before you ask anything) -- If the exe is getting flagged as a virus by your ac it is obviously a false positive, if you still do not trust it you can [compile/freeze](#freezing) it yourself ([VirusTotal Scan](https://www.virustotal.com/gui/file/9df159098638ab8a8bec7205eeb271cb5891c19cdbb81bcd5368dfc1ef213f76/detection)) - -## Usage (script): -- ```pip install -r requirements.txt``` -> use my modified [CTkToolTip](./CTkToolTip) and [CTkListbox](./CTkListbox) for v0.2.8 and up -- ```python T7xwd_package\T7xwd.py``` -- Slap in your workshop item link for example: "https://steamcommunity.com/sharedfiles/filedetails/?id=3011930738" or just the id 3011930738 +## Usage: +- Run [T7xWD.exe](https://github.com/faroukbmiled/T7xWD/releases/latest/download/Release.zip) ([VirusTotal Scan](https://www.virustotal.com/gui/file/5ca1367a82893a1f412b59a52431e9ac4219a67a50c294ee86a7d41473826b14/detection)) +- [Optional] Run as script:```python T7xwd_package\T7xwd.py``` ## Features: -- Improves steamcmd's stability while downloading -- Auto installs mods and maps to T7x +- Improved download stability +- Auto installs mods and maps - Queue -> download items in queue - Library tab -> lists your downloaded items -- Item updater -> Checks your items for updates (redownloads them,no way for now to "update" them only) -> Under Library tab -- Steam to T7x -> Item mover (moves items (mods,maps) from steam to T7x client) -> Under settings tab -- Themes -> Under settings tab -- Bunch of useful settings -> Under settings tab +- Item updater -> checks your items for updates +- Workshop Transfer -> copy/move items from the workshop folder into the game directory +- Custom Themes - -## Freezing into an exe (pyinstaller): -- ```pip install -r requirements.txt``` -> use my modified [CTkToolTip](./CTkToolTip) and [CTkListbox](./CTkListbox) for v0.2.8 and up. +## Notes: +- Steamcmd will be downloaded if it is not installed
+- Initializing SteamCMD for the first time could take some time depending on your internet speed
+ +#### Mouse Bindings: + Library Tab: + + * Mouse1 -> copy id + * Ctrl + Mouse1 -> append to clipboard + * Mouse2 (scroll wheel button) -> open item path in file explorer + * Mouse3 (Right click) -> copy path + +## Building from Source: +- ```pip install -r requirements.txt``` -> use my modified [CTkToolTip](./CTkToolTip) and [CTkListbox](./CTkListbox) - ```python build.py``` - -## Queue tab (beta) - -- added Queue tab which has a text field that you can slap in workshop ids/links in 2 formats, for example:
- - -```3010399939,2976006537,2118338989``` -or
-```3010399939 -2976006537 -2118338989 -2113146805 -``` - -## Hidden Config Options: -- Added a way to update invalid Items (in details) -> add ```update_invalid = yes``` to config.ini -- Added a way to download Beta items that normally will throw invalid item warning -> add ```skip_invalid = no``` to config.ini - - -### Notes: -* It saves your input except for workshop id
-* If you do not know where to find your map in-game check this [video](https://youtu.be/XIQjfXXlgQs?t=260) out ,for mods find "mods" in the game's main menu
-* Initializing SteamCMD for the first time could take some time depending on your internet speed
-* New item update window: Right click on an item (mouse3) -> will open the item in the browser (Steam Workshop)
-* T7xWD requires having "T7xwd_library.json" in the app's directory for the new features to work -> clicking on the library tab will generate the JSON file (please don't touch it)
-* v0.3.1 and up program will use windows registry to save window coordinates (height , width ,x_pos, y_pos) -* For Item Updater to recognize your Item as a valid one is to have it's folder named either "FolderName ,"PublisherID" or "FolderName_PublisherID" (taken from workshop.json from each item) -> Invalid items will have a warning icon in library and will not be checked for updates -> Downloading items from T7xwd will be valid by default -* Invalid items will still work in game but will not be checked for updates - -#### PS (Library tab):
-* Mouse1 -> copy id
-* Mouse2 (scroll wheel btn) -> Open item path in explorer
-* Mouse3 -> Copy path
-* Ctrl + Mouse 1 (after Mouse1) -> Append to clipboard - -### Known bugs:
-* Rare UI bug => instead of showing a warning message, its window goes invisible and leads to the whole ui becoming unclickable (end the task from task manager)
-* Possible logic bugs related to the progress bar , sometimes it carries on progressing when you pressed stop => please raise an issue if this happens often
-* If the exe is getting flagged as a virus by your ac it is obviously a false positive, if you still do not trust it you can [compile/freeze](#freezing) it yourself
-* [VirusTotal](https://www.virustotal.com/gui/file/9df159098638ab8a8bec7205eeb271cb5891c19cdbb81bcd5368dfc1ef213f76/detection)
- -### todos: -- [x] add a menu that shows you current installed mods/maps -- [x] fix the progress bar => progress bar logic based on an estimation -- [x] other improvements regarding the download (steamcmd likes to fail sometimes for no reason) => added a way to keep looping when steamcmd crashes and it will eventually finishes -- [x] add a queue window that you can slap in a bunch of items to download sequentially and or simultaneously -- [ ] add an option to login with your account => delayed (do we really need it?) - -### Themes: -- If you choose "custom" theme a file called T7xwd_theme.json will be created in the current folder (where the exe at) , Don't add anything or edit any keyes just modify the colours, If you mess up something you can just rename the file and it'll go back to the default theme (you can always press custom again and the file will be created again, based on which theme you choose before) diff --git a/build.py b/build.py index 97c519b..f6f4d86 100644 --- a/build.py +++ b/build.py @@ -1,5 +1,4 @@ import os -import shutil import PyInstaller.__main__ from distutils.sysconfig import get_python_lib @@ -29,5 +28,7 @@ PyInstaller.__main__.run([ "--add-data", f"{site_packages_path}/CTkToolTip;CTkToolTip", ]) -current_directory = os.path.dirname(__file__) -shutil.copy2(os.path.join(current_directory, "dist", "T7xWD.exe"), current_directory) +# create symbolic hardlink to main directory +if os.path.exists("T7xWD.exe"): + os.remove("T7xWD.exe") +os.link('dist/T7xWD.exe', 'T7xWD.exe') diff --git a/requirements.txt b/requirements.txt index f6efb1d..9dfd771 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/t7xwd_package/src/helpers.py b/t7xwd_package/src/helpers.py index 11a0e8d..a0707ae 100644 --- a/t7xwd_package/src/helpers.py +++ b/t7xwd_package/src/helpers.py @@ -3,12 +3,13 @@ from src.imports import * # Start helper functions -#testing app offline +# testing app offline # import socket # def guard(*args, **kwargs): # pass # socket.socket = guard + def check_config(name, fallback=None): config = configparser.ConfigParser() config.read(CONFIG_FILE_PATH) @@ -16,6 +17,7 @@ def check_config(name, fallback=None): return config.get("Settings", name, fallback=fallback) return config.get("Settings", name, fallback="") + def save_config(name, value): config = configparser.ConfigParser() config.read(CONFIG_FILE_PATH) @@ -24,25 +26,32 @@ def save_config(name, value): with open(CONFIG_FILE_PATH, "w") as config_file: config.write(config_file) + def check_custom_theme(theme_name): if os.path.exists(os.path.join(APPLICATION_PATH, theme_name)): return os.path.join(APPLICATION_PATH, theme_name) else: - try: return os.path.join(RESOURCES_DIR, theme_name) - except: return os.path.join(RESOURCES_DIR, "T7xwd_theme.json") + try: + return os.path.join(RESOURCES_DIR, theme_name) + except: + return os.path.join(RESOURCES_DIR, "T7xwd_theme.json") + # theme initialization -ctk.set_appearance_mode(check_config("appearance", "Dark")) # Modes: "System" (standard), "Dark", "Light" +# Modes: "System" (standard), "Dark", "Light" +ctk.set_appearance_mode(check_config("appearance", "Dark")) try: - ctk.set_default_color_theme(check_custom_theme(check_config("theme", fallback="T7xwd_theme.json"))) + ctk.set_default_color_theme(check_custom_theme( + check_config("theme", fallback="T7xwd_theme.json"))) except: save_config("theme", "T7xwd_theme.json") - ctk.set_default_color_theme(os.path.join(RESOURCES_DIR, "T7xwd_theme.json")) + ctk.set_default_color_theme(os.path.join( + RESOURCES_DIR, "T7xwd_theme.json")) + def get_latest_release_version(): try: - release_api_url = f"https://git.rimmyscorner.com/api/v1/repos/{GITHUB_REPO}/releases/latest" - + release_api_url = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest" response = requests.get(release_api_url) response.raise_for_status() data = response.json() @@ -51,6 +60,7 @@ def get_latest_release_version(): show_message("Warning", f"Error while checking for updates: \n{e}") return None + def create_update_script(current_exe, new_exe, updater_folder, program_name): script_content = f""" @echo off @@ -76,6 +86,7 @@ def create_update_script(current_exe, new_exe, updater_folder, program_name): return script_path + def if_internet_available(func): if func == "return": try: @@ -83,23 +94,28 @@ def if_internet_available(func): return True except: return False + def wrapper(*args, **kwargs): try: requests.get("https://www.google.com", timeout=3) return func(*args, **kwargs) except: - show_message("Offline", "No internet connection. Please check your internet connection and try again.") + show_message( + "Offline", "No internet connection. Please check your internet connection and try again.") return return wrapper + @if_internet_available def check_for_updates_func(window, ignore_up_todate=False): try: latest_version = get_latest_release_version() current_version = VERSION - int_latest_version = int(latest_version.replace("v", "").replace(".", "")) - int_current_version = int(current_version.replace("v", "").replace(".", "")) + int_latest_version = int( + latest_version.replace("v", "").replace(".", "")) + int_current_version = int( + current_version.replace("v", "").replace(".", "")) if latest_version and int_latest_version > int_current_version: msg_box = CTkMessagebox(title="Update Available", message=f"An update is available! Install now?\n\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="View", option_2="No", option_3="Yes", fade_in_duration=int(1), sound=True) @@ -107,7 +123,8 @@ def check_for_updates_func(window, ignore_up_todate=False): result = msg_box.get() if result == "View": - webbrowser.open(f"https://git.rimmyscorner.com/{GITHUB_REPO}/releases/latest") + webbrowser.open( + f"https://github.com/{GITHUB_REPO}/releases/latest") if result == "Yes": from src.update_window import UpdateWindow @@ -125,15 +142,18 @@ def check_for_updates_func(window, ignore_up_todate=False): elif int_latest_version == int_current_version: if ignore_up_todate: return - msg_box = CTkMessagebox(title="Up to Date!", message="No Updates Available!", option_1="Ok", sound=True) + msg_box = CTkMessagebox( + title="Up to Date!", message="No Updates Available!", option_1="Ok", sound=True) result = msg_box.get() else: - show_message("Error!", "An error occured while checking for updates!\nCheck your internet and try again") + show_message( + "Error!", "An error occured while checking for updates!\nCheck your internet and try again") except Exception as e: show_message("Error", f"Error while checking for updates: \n{e}", icon="cancel") + def extract_workshop_id(link): try: pattern = r'(?<=id=)(\d+)' @@ -146,6 +166,7 @@ def extract_workshop_id(link): except: return None + def check_steamcmd(): steamcmd_path = get_steamcmd_path() steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe") @@ -155,25 +176,31 @@ def check_steamcmd(): return True + def initialize_steam(master): try: steamcmd_path = get_steamcmd_path() steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe") - process = subprocess.Popen([steamcmd_exe_path, "+quit"], creationflags=subprocess.CREATE_NEW_CONSOLE) + process = subprocess.Popen( + [steamcmd_exe_path, "+quit"], creationflags=subprocess.CREATE_NEW_CONSOLE) master.attributes('-alpha', 0.0) process.wait() if is_steamcmd_initialized(): - show_message("SteamCMD has terminated!", "T7xWD is ready for action.", icon="info") + show_message("SteamCMD has terminated!", + "T7xWD is ready for action.", icon="info") else: - show_message("SteamCMD has terminated!!", "SteamCMD isn't initialized yet") + show_message("SteamCMD has terminated!!", + "SteamCMD isn't initialized yet") except: - show_message("Error!", "An error occurred please check your paths and try again.", icon="cancel") + show_message( + "Error!", "An error occurred please check your paths and try again.", icon="cancel") master.attributes('-alpha', 1.0) + @if_internet_available def valid_id(workshop_id): data = item_steam_api(workshop_id) - if check_config("skip_invalid", "skip") == "no": + if check_config("skip_invalid", "on") == "off": if data: return True if "consumer_app_id" in data['response']['publishedfiledetails'][0]: @@ -181,6 +208,7 @@ def valid_id(workshop_id): else: return False + def convert_speed(speed_bytes): if speed_bytes < 1024: return speed_bytes, "B/s" @@ -191,27 +219,31 @@ def convert_speed(speed_bytes): else: return speed_bytes / (1024 * 1024 * 1024), "GB/s" + def create_default_config(): config = configparser.ConfigParser() config["Settings"] = { "SteamCMDPath": APPLICATION_PATH, "DestinationFolder": "", - "checkforupdtes": "on", + "checkforupdates": "on", "console": "off" } with open(CONFIG_FILE_PATH, "w") as config_file: config.write(config_file) + def get_steamcmd_path(): config = configparser.ConfigParser() config.read(CONFIG_FILE_PATH) return config.get("Settings", "SteamCMDPath", fallback=APPLICATION_PATH) + def extract_json_data(json_path, key): with open(json_path, 'r') as json_file: data = json.load(json_file) return data.get(key, '') + def convert_bytes_to_readable(size_in_bytes, no_symb=None): for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if size_in_bytes < 1024.0: @@ -220,6 +252,7 @@ def convert_bytes_to_readable(size_in_bytes, no_symb=None): return f"{size_in_bytes:.2f} {unit}" size_in_bytes /= 1024.0 + def get_workshop_file_size(workshop_id, raw=None): url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}&searchtext=" response = requests.get(url) @@ -230,23 +263,34 @@ def get_workshop_file_size(workshop_id, raw=None): if raw: file_size_text = file_size_element.get_text(strip=True) file_size_text = file_size_text.replace(",", "") - file_size_in_mb = float(file_size_text.replace(" MB", "")) - file_size_in_bytes = int(file_size_in_mb * 1024 * 1024) + if "GB" in file_size_text: + file_size_in_gb = float(file_size_text.replace(" GB", "")) + file_size_in_bytes = int(file_size_in_gb * 1024 * 1024 * 1024) + else: + file_size_in_mb = float(file_size_text.replace(" MB", "")) + file_size_in_bytes = int(file_size_in_mb * 1024 * 1024) return convert_bytes_to_readable(file_size_in_bytes) if file_size_element: file_size_text = file_size_element.get_text(strip=True) file_size_text = file_size_text.replace(",", "") - file_size_in_mb = float(file_size_text.replace(" MB", "")) - file_size_in_bytes = int(file_size_in_mb * 1024 * 1024) + if "GB" in file_size_text: + file_size_in_gb = float(file_size_text.replace(" GB", "")) + file_size_in_bytes = int(file_size_in_gb * 1024 * 1024 * 1024) + else: + file_size_in_mb = float(file_size_text.replace(" MB", "")) + file_size_in_bytes = int(file_size_in_mb * 1024 * 1024) return file_size_in_bytes return None - except: + except Exception as e: + print(e) return None + def show_message(title, message, icon="warning", _return=False, option_1="No", option_2="Ok"): if _return: - msg = CTkMessagebox(title=title, message=message, icon=icon, option_1=option_1, option_2=option_2, sound=True) + msg = CTkMessagebox(title=title, message=message, icon=icon, + option_1=option_1, option_2=option_2, sound=True) response = msg.get() if response == option_1: return False @@ -259,18 +303,22 @@ def show_message(title, message, icon="warning", _return=False, option_1="No", o CTkMessagebox(title=title, message=message, icon=icon, sound=True) main_app.app.after(0, callback) -def launch_T7x_func(path): - procname = "t7x.exe" + +def launch_game_func(path, procname="BlackOps3.exe", flags=""): + if not procname.endswith(('.exe', '.bat', '.sh', '.lnk')): + procname += '.exe' try: if procname in (p.name() for p in psutil.process_iter()): for proc in psutil.process_iter(): if proc.name() == procname: proc.kill() - T7x_path = os.path.join(path, procname) - subprocess.Popen([T7x_path ,"-launch"] , cwd=path) - show_message("Please wait!", "The game has launched in the background it will open up in a sec!", icon="info") + game_path = os.path.join(path, procname) + subprocess.Popen([game_path, flags], cwd=path) + show_message( + "Please wait!", "The game has launched in the background it will open up in a sec!", icon="info") except Exception as e: - show_message("Error: Failed to launch T7x", f"Failed to launch t7x.exe\nMake sure to put in your correct T7x path\n{e}") + show_message(f"Error: Failed to launch game", f"Failed to start {procname}\n\nMake sure the game path is correct.\n\n{e}") + def remove_tree(folder_path, show_error=None): if show_error: @@ -283,11 +331,13 @@ def remove_tree(folder_path, show_error=None): except Exception as e: pass + def convert_seconds(seconds): minutes, seconds = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) return hours, minutes, seconds + def get_folder_size(folder_path): total_size = 0 for path, dirs, files in os.walk(folder_path): @@ -296,6 +346,7 @@ def get_folder_size(folder_path): total_size += os.stat(fp).st_size return total_size + def is_steamcmd_initialized(): steamcmd_path = get_steamcmd_path() steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe") @@ -304,6 +355,7 @@ def is_steamcmd_initialized(): return False return True + def get_button_state_colors(file_path, state): try: with open(file_path, 'r') as json_file: @@ -321,10 +373,12 @@ def get_button_state_colors(file_path, state): except json.JSONDecodeError: return None + def reset_steamcmd(no_warn=None): steamcmd_path = get_steamcmd_path() - directories_to_reset = ["steamapps", "dumps", "logs", "depotcache", "appcache","userdata",] + directories_to_reset = ["steamapps", "dumps", + "logs", "depotcache", "appcache", "userdata",] for directory in directories_to_reset: directory_path = os.path.join(steamcmd_path, directory) @@ -338,10 +392,13 @@ def reset_steamcmd(no_warn=None): os.remove(file_path) if not no_warn: - show_message("Success!", "SteamCMD has been reset successfully!", icon="info") + show_message( + "Success!", "SteamCMD has been reset successfully!", icon="info") else: if not no_warn: - show_message("Warning!", "steamapps folder was not found, maybe already removed?", icon="warning") + show_message( + "Warning!", "steamapps folder was not found, maybe already removed?", icon="warning") + def get_item_name(id): try: @@ -355,8 +412,52 @@ def get_item_name(id): return False # you gotta use my modded CTkToolTip originaly by Akascape -def show_noti(widget ,message, event=None, noti_dur=3.0, topmost=False): - CTkToolTip(widget, message=message, is_noti=True, noti_event=event, noti_dur=noti_dur, topmost=topmost) + + +def show_noti(widget, message, event=None, noti_dur=3.0, topmost=False): + CTkToolTip(widget, message=message, is_noti=True, + noti_event=event, noti_dur=noti_dur, topmost=topmost) + + +def get_update_time_from_html(workshop_id): + current_year = datetime.now().year + date_format_with_year = "%d %b, %Y @ %I:%M%p" + date_format_with_added_year = "%d %b @ %I:%M%p, %Y" + url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}" + + try: + response = requests.get(url) + response.raise_for_status() + content = response.text + + soup = BeautifulSoup(content, "html.parser") + + details_stats_container = soup.find( + "div", class_="detailsStatsContainerRight") + if details_stats_container: + details_stat_elements = details_stats_container.find_all( + "div", class_="detailsStatRight") + try: + date_updated = details_stat_elements[2].text.strip() + except: + date_updated = None + + if not date_updated: + return None + + try: + date_updated = datetime.strptime( + date_updated, date_format_with_year) + except ValueError: + date_updated = datetime.strptime( + date_updated + f", {current_year}", date_format_with_added_year) + + return int(date_updated.timestamp()) + + except Exception as e: + print(f"Error getting update time for URL {url}: {e}") + return None + def check_item_date(down_date, date_updated, format=False): current_year = datetime.now().year @@ -364,15 +465,19 @@ def check_item_date(down_date, date_updated, format=False): date_format_with_added_year = "%d %b @ %I:%M%p, %Y" try: try: - download_datetime = datetime.strptime(down_date, date_format_with_year) + download_datetime = datetime.strptime( + down_date, date_format_with_year) except ValueError: - download_datetime = datetime.strptime(down_date + f", {current_year}", date_format_with_added_year) + download_datetime = datetime.strptime( + down_date + f", {current_year}", date_format_with_added_year) if format: try: - date_updated = datetime.strptime(date_updated, date_format_with_year) + date_updated = datetime.strptime( + date_updated, date_format_with_year) except ValueError: - date_updated = datetime.strptime(date_updated + f", {current_year}", date_format_with_added_year) + date_updated = datetime.strptime( + date_updated + f", {current_year}", date_format_with_added_year) if date_updated >= download_datetime: return True @@ -381,6 +486,7 @@ def check_item_date(down_date, date_updated, format=False): except: return False + def get_window_size_from_registry(): try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, REGISTRY_KEY_PATH, 0, winreg.KEY_READ) as key: @@ -392,16 +498,19 @@ def get_window_size_from_registry(): except (FileNotFoundError, OSError, ValueError, FileNotFoundError): return None, None, None, None + def save_window_size_to_registry(width, height, x, y): try: with winreg.CreateKey(winreg.HKEY_CURRENT_USER, REGISTRY_KEY_PATH) as key: winreg.SetValueEx(key, "WindowWidth", 0, winreg.REG_SZ, str(width)) - winreg.SetValueEx(key, "WindowHeight", 0, winreg.REG_SZ, str(height)) + winreg.SetValueEx(key, "WindowHeight", 0, + winreg.REG_SZ, str(height)) winreg.SetValueEx(key, "WindowX", 0, winreg.REG_SZ, str(x)) winreg.SetValueEx(key, "WindowY", 0, winreg.REG_SZ, str(y)) except Exception as e: print(f"Error saving to registry: {e}") + def item_steam_api(id): try: url = ITEM_INFO_API @@ -416,6 +525,7 @@ def item_steam_api(id): print(e) return False + def get_item_dates(ids): try: data = { @@ -429,12 +539,44 @@ def get_item_dates(ids): if "response" in response_data: item_details = response_data["response"]["publishedfiledetails"] - return {item["publishedfileid"]: item["time_updated"] for item in item_details} + return_list = {item["publishedfileid"]: item.get( + "time_updated", None) for item in item_details} + return return_list return {} except Exception as e: - print(e) + print("Error: could not fetch all update times. Breaking early.") return {} + +def isNullOrWhiteSpace(str): + if (str is None) or (str == "") or (str.isspace()): + return True + return False + + +def concatenate_sublists(a): + out = [] + for sublist in a: + out.extend(sublist) + return out + + +def nextnonexistentdir(f, dir=os.path.dirname(os.path.realpath(__file__))): + i = 0 + + def already_exists(i): + if i==0 and os.path.exists(os.path.join(dir, (f).strip())): + return True + else: + return os.path.exists(os.path.join(dir, (f + f'_{str(i)}').strip())) + + while already_exists(i): + i += 1 + + root_i_ext = [f, i] + + return root_i_ext + # End helper functions diff --git a/t7xwd_package/src/imports.py b/t7xwd_package/src/imports.py index 28ade78..8474d81 100644 --- a/t7xwd_package/src/imports.py +++ b/t7xwd_package/src/imports.py @@ -46,4 +46,4 @@ LIBRARY_FILE = "T7xwd_library.json" RESOURCES_DIR = os.path.join(os.path.dirname(__file__), '..', 'resources') UPDATER_FOLDER = "update" REGISTRY_KEY_PATH = r"Software\T7xWD" -VERSION = "v0.3.3" \ No newline at end of file +VERSION = "v0.3.5" \ No newline at end of file diff --git a/t7xwd_package/src/library_tab.py b/t7xwd_package/src/library_tab.py index b473155..814fe89 100644 --- a/t7xwd_package/src/library_tab.py +++ b/t7xwd_package/src/library_tab.py @@ -231,7 +231,7 @@ class LibraryTab(ctk.CTkScrollableFrame): item_type, item_name = item[5], item[0] return (0, item_name) if item_type == "map" else (1, item_name) - def load_items(self, T7xFolder, dont_add=False): + def load_items(self, gameFolder, dont_add=False): if self.refresh_next_time and not dont_add: self.refresh_next_time = False status = self.refresh_items() @@ -243,8 +243,8 @@ class LibraryTab(ctk.CTkScrollableFrame): self.ids_added.clear() self.refresh_next_time = True - maps_folder = Path(T7xFolder) / "mods" - mods_folder = Path(T7xFolder) / "usermaps" + maps_folder = Path(gameFolder) / "mods" + mods_folder = Path(gameFolder) / "usermaps" mod_img = os.path.join(RESOURCES_DIR, "mod_image.png") map_img = os.path.join(RESOURCES_DIR, "map_image.png") b_mod_img = os.path.join(RESOURCES_DIR, "b_mod_image.png") @@ -262,14 +262,14 @@ class LibraryTab(ctk.CTkScrollableFrame): except: pass for folder_path in folders_to_process: - for zone_path in folder_path.glob("**/zone"): + for zone_path in folder_path.glob("*/zone"): json_path = zone_path / "workshop.json" if json_path.exists(): curr_folder_name = zone_path.parent.name workshop_id = extract_json_data(json_path, "PublisherID") or "None" name = re.sub(r'\^\d', '', extract_json_data(json_path, "Title")) or "None" - name = name[:45] + "..." if len(name) > 45 else name + name = name[:60] + "..." if len(name) > 60 else name item_type = extract_json_data(json_path, "Type") or "None" folder_name = extract_json_data(json_path, "FolderName") or "None" folder_size_bytes = get_folder_size(zone_path.parent) @@ -298,7 +298,7 @@ class LibraryTab(ctk.CTkScrollableFrame): if curr_folder_name not in self.added_folders: image_path = mod_img if item_type == "mod" else map_img if not (str(curr_folder_name).strip() == str(workshop_id).strip() or str(curr_folder_name).strip() == str(folder_name).strip() - or str(curr_folder_name).strip() == f"{folder_name}_{workshop_id}"): + or str(curr_folder_name).strip() == f"{folder_name}_{workshop_id}" or str(curr_folder_name).strip() == f"{folder_name}_duplicated"): try: self.remove_item_by_option(items_file, curr_folder_name, "folder_name") except: pass self.item_block_list.add(curr_folder_name) @@ -308,6 +308,7 @@ class LibraryTab(ctk.CTkScrollableFrame): elif (curr_folder_name not in self.added_folders and (workshop_id in self.ids_added or workshop_id == "None")): try: self.remove_item_by_option(items_file, curr_folder_name, "folder_name") except: pass + self.item_block_list.add(workshop_id) text_to_add = re.sub(r'ID:\s+(?:\d+|None)', f'Folder: {curr_folder_name}', text_to_add) image_path = b_mod_img if item_type == "mod" else b_map_img text_to_add += " | ⚠️" @@ -325,7 +326,7 @@ class LibraryTab(ctk.CTkScrollableFrame): "folder_name": curr_folder_name, "json_folder_name": folder_name } - # when item is blocked ,item_exists_in_file() returns None for folder_found + # when item is blocked item_exists_in_file() returns None for folder_found if not id_found and folder_found == None: self.remove_item_by_option(items_file, curr_folder_name, "folder_name") elif not id_found and not folder_found and curr_folder_name not in self.item_block_list and workshop_id not in self.ids_added: @@ -339,14 +340,14 @@ class LibraryTab(ctk.CTkScrollableFrame): f.seek(0) json.dump(items_data, f, indent=4) - if id_found and not folder_found and curr_folder_name not in self.item_block_list and workshop_id not in self.ids_added: + if curr_folder_name not in self.item_block_list: self.update_or_add_item_by_id(items_file, item_info, workshop_id) # keep here cuz of item_exists_in_file() testing self.added_folders.add(curr_folder_name) # added that cuz it sometimes can add blocked ids first # and legit ids will be blocked cuz theyll be added to "ids_added" - if not workshop_id in self.ids_added and curr_folder_name not in self.item_block_list: + if not workshop_id in self.ids_added and curr_folder_name not in self.item_block_list and workshop_id!='None': self.ids_added.add(workshop_id) # sort items by type then alphabet @@ -354,7 +355,7 @@ class LibraryTab(ctk.CTkScrollableFrame): for item in ui_items_to_add: self.add_item_helper(*item) - if not self.file_cleaned and os.path.exists(items_file): + if os.path.exists(items_file): self.file_cleaned = True self.clean_json_file(items_file) @@ -372,22 +373,22 @@ class LibraryTab(ctk.CTkScrollableFrame): self.show_no_items_message(only_up=True) return "No items in current selected folder" - def update_item(self, T7xFolder, id, item_type, foldername): + def update_item(self, gameFolder, id, item_type, foldername): try: if item_type == "map": - folder_path = Path(T7xFolder) / "usermaps" / f"{foldername}" + folder_path = Path(gameFolder) / "usermaps" / f"{foldername}" elif item_type == "mod": - folder_path = Path(T7xFolder) / "mods" / f"{foldername}" + folder_path = Path(gameFolder) / "mods" / f"{foldername}" else: raise ValueError("Unsupported item_type. It must be 'map' or 'mod'.") - for zone_path in folder_path.glob("**/zone"): + for zone_path in folder_path.glob("*/zone"): json_path = zone_path / "workshop.json" if json_path.exists(): workshop_id = extract_json_data(json_path, "PublisherID") if workshop_id == id: name = extract_json_data(json_path, "Title").replace(">", "").replace("^", "") - name = name[:45] + "..." if len(name) > 45 else name + name = name[:60] + "..." if len(name) > 60 else name item_type = extract_json_data(json_path, "Type") folder_name = extract_json_data(json_path, "FolderName") size = convert_bytes_to_readable(get_folder_size(zone_path.parent)) @@ -456,7 +457,8 @@ class LibraryTab(ctk.CTkScrollableFrame): self.added_items.clear() self.added_folders.clear() self.ids_added.clear() - status = self.load_items(main_app.app.edit_destination_folder.get().strip()) + + status = self.load_items( main_app.app.settings_tab.edit_destination_folder.get().strip()) main_app.app.title(f"T7x Workshop Downloader - Library ➜ {status}") # main_app library event needs a return for status => when refresh_next_time is true return status @@ -471,7 +473,7 @@ class LibraryTab(ctk.CTkScrollableFrame): if only_up: return self.no_items_label.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="n") - self.no_items_label.configure(text="No items found in the selected folder. \nMake sure you have a mod/map downloaded and or have the right T7x folder selected.") + self.no_items_label.configure(text="No items found in the selected folder. \nMake sure you have a mod/map downloaded and or have the right game folder selected.") def hide_no_items_message(self): self.update_tooltip.configure(message="Check items for updates") @@ -587,7 +589,7 @@ class LibraryTab(ctk.CTkScrollableFrame): if json_path.exists(): workshop_id = extract_json_data(json_path, "PublisherID") or "None" name = re.sub(r'\^\w+', '', extract_json_data(json_path, "Title")) or "None" - map_name = name[:45] + "..." if len(name) > 45 else name + map_name = name[:60] + "..." if len(name) > 60 else name map_mod_type = extract_json_data(json_path, "Type") or "None" preview_iamge = json_path.parent / "previewimage.png" if preview_iamge.exists(): @@ -693,7 +695,7 @@ class LibraryTab(ctk.CTkScrollableFrame): main_app.app.edit_workshop_id.delete(0, "end") main_app.app.edit_workshop_id.insert(0, workshop_id) main_app.app.main_button_event() - if invalid_warn and check_config("update_invalid", "no") == "yes": + if invalid_warn and check_config("update_invalid", "off") == "on": main_app.app.download_map(update=True, invalid_item_folder=os.path.basename(folder)) else: main_app.app.download_map(update=True) @@ -810,8 +812,8 @@ class LibraryTab(ctk.CTkScrollableFrame): update_btn.configure(state="disabled") update_btn_tooltip.configure(message="Currently offline") view_button_tooltip.configure(message="Currently offline") - if check_config("update_invalid", "no") == "yes": - update_btn_tooltip.configure(message="update_invalid is set to 'yes' in config.ini") + if check_config("update_invalid", "off") == "on": + update_btn_tooltip.configure(message="update_invalid is set to 'on' in config.ini") elif invalid_warn: update_btn.configure(text="Update", state="disabled") update_btn_tooltip.configure(message="Disabled due to item being blocked or duplicated") @@ -869,12 +871,19 @@ class LibraryTab(ctk.CTkScrollableFrame): item_data = get_item_dates(item_ids) for item_id, date_updated in item_data.items(): - item_date = item_dates[item_id] + if not date_updated: + try: + new_date = get_update_time_from_html(item_id) + date_updated = new_date if new_date else 1 + except: + date_updated = 1 + item_date = item_dates[str(item_id)] if str(item_id) in item_dates else "" date_updated = datetime.fromtimestamp(date_updated) if check_item_date(item_date, date_updated): - date_updated = date_updated.strftime("%d %b @ %I:%M%p, %Y") - self.to_update.add(texts[item_id] + f" | Updated: {date_updated}") + if item_date != "": + date_updated = date_updated.strftime("%d %b @ %I:%M%p, %Y") + self.to_update.add(texts[item_id] + f" | Updated: {date_updated}") except Exception as e: show_message("Error", f"Error occurred\n{e}", icon="cancel") @@ -884,7 +893,7 @@ class LibraryTab(ctk.CTkScrollableFrame): lib_data = None if not os.path.exists(os.path.join(APPLICATION_PATH, LIBRARY_FILE)): - show_message("Error checking for item updates! -> Setting is on", "Please visit library tab at least once with the correct T7x path!, you also need to have at least 1 item!") + show_message("Error checking for item updates! -> Setting is on", "Please visit library tab at least once with the correct game path! You also need to have at least 1 item!") return with open(os.path.join(APPLICATION_PATH, LIBRARY_FILE), 'r') as file: @@ -897,7 +906,7 @@ class LibraryTab(ctk.CTkScrollableFrame): if_ids_need_update(item_ids, item_dates, texts) except: - show_message("Error checking for item updates!", "Please visit the library tab at least once with the correct T7x path!, you also need to have at least 1 item!") + show_message("Error checking for item updates!", "Please visit the library tab at least once with the correct game path! You also need to have at least 1 item!") return check_for_update() diff --git a/t7xwd_package/src/main.py b/t7xwd_package/src/main.py index 97eb5cf..3abaf28 100644 --- a/t7xwd_package/src/main.py +++ b/t7xwd_package/src/main.py @@ -23,37 +23,40 @@ class T7xWD(ctk.CTk): self.geometry(f"{920}x{560}") self.minsize(920, 560) + ctk.set_window_scaling(float(check_config("scaling", 1.0)) + 0.25) if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")): self.wm_iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")) self.protocol("WM_DELETE_WINDOW", self.on_closing) - # Qeue frame/tab, keep here or app will start shrinked eveytime - self.qeueuframe = ctk.CTkFrame(self) - self.qeueuframe.columnconfigure(1, weight=1) - self.qeueuframe.columnconfigure(2, weight=1) - self.qeueuframe.columnconfigure(3, weight=1) - self.qeueuframe.rowconfigure(1, weight=1) - self.qeueuframe.rowconfigure(2, weight=1) - self.qeueuframe.rowconfigure(3, weight=1) - self.qeueuframe.rowconfigure(4, weight=1) + # Queue frame/tab, keep here or app will start shrinked eveytime + self.queue_tab_enabled = False # Enable Queue Tab - self.workshop_queue_label = ctk.CTkLabel(self.qeueuframe, text="Workshop IDs/Links -> press help to see examples:") - self.workshop_queue_label.grid(row=0, column=0, padx=(20, 20), pady=(20, 20), sticky="wns") + self.queueframe = ctk.CTkFrame(self) + self.queueframe.columnconfigure(1, weight=1) + self.queueframe.columnconfigure(2, weight=1) + self.queueframe.columnconfigure(3, weight=1) + self.queueframe.rowconfigure(1, weight=1) + self.queueframe.rowconfigure(2, weight=0) + self.queueframe.rowconfigure(3, weight=0) + self.queueframe.rowconfigure(4, weight=0) - self.help_button = ctk.CTkButton(master=self.qeueuframe, text="Help", command=self.help_queue_text_func, width=10, height=10, fg_color="#585858") - self.help_button.grid(row=0, column=0, padx=(352, 0), pady=(23, 0), sticky="en") + self.workshop_queue_label = ctk.CTkLabel(self.queueframe, text="Batch Downloader:") + self.workshop_queue_label.grid(row=0, column=0, padx=(20, 20), pady=(10, 10), sticky="wns") + + self.help_button = ctk.CTkButton(master=self.queueframe, text="Help", command=self.help_queue_text_func, width=10, height=10, fg_color="#585858") + self.help_button.grid(row=0, column=3, padx=(0, 20), pady=(10, 0), sticky="en") self.help_restore_content = None - self.queuetextarea = ctk.CTkTextbox(master=self.qeueuframe, font=("", 15)) - self.queuetextarea.grid(row=1, column=0, columnspan=4, padx=(20, 20), pady=(0, 20), sticky="nwse") + self.queuetextarea = ctk.CTkTextbox(master=self.queueframe, font=("", 15)) + self.queuetextarea.grid(row=1, column=0, columnspan=4, padx=(20, 20), pady=(0, 40), sticky="nwse") - self.status_text = ctk.CTkLabel(self.qeueuframe, text="Status: Standby!") - self.status_text.grid(row=3, column=0, padx=(20, 20), pady=(0, 20), sticky="ws") + self.status_text = ctk.CTkLabel(self.queueframe, text="Status: Standby!") + self.status_text.grid(row=1, column=0, padx=(20, 20), pady=(40, 0), sticky="ws") - self.skip_boutton = ctk.CTkButton(master=self.qeueuframe, text="Skip", command=self.skip_current_queue_item, width=10, height=10, fg_color="#585858") + self.skip_button = ctk.CTkButton(master=self.queueframe, text="Skip", command=self.skip_current_queue_item, width=10, height=10, fg_color="#585858") - self.qeueuframe.grid_remove() + self.queueframe.grid_remove() # configure grid layout (4x4) self.grid_columnconfigure(1, weight=1) @@ -78,18 +81,22 @@ class T7xWD(ctk.CTk): self.txt_label.grid(row=1, column=0, padx=20, pady=(20, 10)) self.sidebar_main = ctk.CTkButton(self.sidebar_frame, height=28) self.sidebar_main.grid(row=2, column=0, padx=10, pady=(20, 6)) - self.sidebar_queue = ctk.CTkButton(self.sidebar_frame, height=28) - self.sidebar_queue.grid(row=3, column=0, padx=10, pady=6) + if self.queue_tab_enabled: + self.sidebar_queue = ctk.CTkButton(self.sidebar_frame, height=28) + self.sidebar_queue.grid(row=3, column=0, padx=10, pady=6) self.sidebar_library = ctk.CTkButton(self.sidebar_frame, height=28) self.sidebar_library.grid(row=4, column=0, padx=10, pady=6, sticky="n") + self.launch_game = ctk.CTkButton(self.sidebar_frame, text="Launch game", command=self.settings_launch_game, height=28) + self.launch_game.grid(row=5, column=0, padx=10, pady=6, sticky="n") self.sidebar_settings = ctk.CTkButton(self.sidebar_frame, height=28) - self.sidebar_settings.grid(row=5, column=0, padx=10, pady=6, sticky="n") + self.sidebar_settings.grid(row=6, column=0, padx=10, pady=6, sticky="n") # create optionsframe self.optionsframe = ctk.CTkFrame(self) - self.optionsframe.grid(row=0, column=1, rowspan=2, padx=(0, 20), pady=(20, 0), sticky="nsew") + self.optionsframe.grid(row=0, column=1, rowspan=1, padx=(0, 20), pady=(20, 0), sticky="nsew") self.txt_main = ctk.CTkLabel(self.optionsframe, text="💎 T7xWD 💎", font=(font, 20)) - self.txt_main.grid(row=0, column=1, columnspan=5, padx=0, pady=(20, 20), sticky="n") + self.txt_main.grid(row=0, column=1, columnspan=1, padx=0, pady=(20, 20), sticky="n") + self.queueframe.grid(row=1, column=1, rowspan=1, padx=(0, 20), pady=(20, 0), sticky="nsew") # create slider and progressbar frame self.slider_progressbar_frame = ctk.CTkFrame(self) @@ -103,9 +110,6 @@ class T7xWD(ctk.CTk): self.slider_progressbar_frame.rowconfigure(2, weight=1) self.slider_progressbar_frame.rowconfigure(3, weight=1) - # self.spacer = ctk.CTkLabel(master=self.slider_progressbar_frame, text="") - # self.spacer.grid(row=0, column=0, columnspan=1) - self.label_speed = ctk.CTkLabel(master=self.slider_progressbar_frame, text="Awaiting Download!") self.label_speed.grid(row=1, column=0, padx=20, pady=(0, 10), sticky="w") self.elapsed_time = ctk.CTkLabel(master=self.slider_progressbar_frame, text="", anchor="center") @@ -127,79 +131,61 @@ class T7xWD(ctk.CTk): # options frame self.optionsframe.columnconfigure(1, weight=1) - self.optionsframe.columnconfigure(2, weight=1) - self.optionsframe.columnconfigure(3, weight=1) - self.optionsframe.rowconfigure(1, weight=1) - self.optionsframe.rowconfigure(2, weight=1) - self.optionsframe.rowconfigure(3, weight=1) - self.optionsframe.rowconfigure(4, weight=1) + self.optionsframe.columnconfigure(2, weight=0) + self.optionsframe.columnconfigure(3, weight=0) + self.optionsframe.rowconfigure(1, weight=0) + self.optionsframe.rowconfigure(2, weight=0) + self.optionsframe.rowconfigure(3, weight=0) + self.optionsframe.rowconfigure(4, weight=0) - self.label_workshop_id = ctk.CTkLabel(master=self.optionsframe, text="Enter the Workshop ID or Link of the map/mod you want to download:\n") - self.label_workshop_id.grid(row=1, column=1, padx=20, pady=(10, 0), columnspan=4, sticky="ws") + self.label_workshop_id = ctk.CTkLabel(master=self.optionsframe, text="Enter a Workshop ID or Link to download:\n") + self.label_workshop_id.grid(row=1, column=1, padx=(10, 5), pady=(10, 0), columnspan=1, sticky="ws") self.check_if_changed = ctk.StringVar() - self.check_if_changed.trace_add("write", self.id_chnaged_handler) + self.check_if_changed.trace_add("write", self.id_changed_handler) self.edit_workshop_id = ctk.CTkEntry(master=self.optionsframe, textvariable=self.check_if_changed) - self.edit_workshop_id.grid(row=2, column=1, padx=20, pady=(0, 10), columnspan=4, sticky="ewn") + self.edit_workshop_id.grid(row=2, column=1, padx=(5, 5), pady=(0, 10), columnspan=1, sticky="ewn") self.button_browse = ctk.CTkButton(master=self.optionsframe, text="Workshop", command=self.open_browser, width=10) - self.button_browse.grid(row=2, column=5, padx=(0, 20), pady=(0, 10), sticky="en") - self.button_browse_tooltip = CTkToolTip(self.button_browse, message="Will open steam workshop for T7x in your browser") + self.button_browse.grid(row=2, column=2, padx=(5, 5), pady=(0, 10), sticky="en") + self.button_browse_tooltip = CTkToolTip(self.button_browse, message="Will open steam workshop in your browser") self.info_button = ctk.CTkButton(master=self.optionsframe, text="Details", command=self.show_map_info, width=10) - self.info_button.grid(row=2, column=5, padx=(0, 20), pady=(0, 10), sticky="wn") - - self.label_destination_folder = ctk.CTkLabel(master=self.optionsframe, text='Enter Your T7x folder:') - self.label_destination_folder.grid(row=3, column=1, padx=20, pady=(0, 0), columnspan=4, sticky="ws") - - self.edit_destination_folder = ctk.CTkEntry(master=self.optionsframe, placeholder_text="Your T7x Instalation folder") - self.edit_destination_folder.grid(row=4, column=1, padx=20, pady=(0, 25), columnspan=4, sticky="ewn") - - self.button_T7x_browse = ctk.CTkButton(master=self.optionsframe, text="Select", command=self.open_T7x_browser) - self.button_T7x_browse.grid(row=4, column=5, padx=(0, 20), pady=(0, 10), sticky="ewn") - - self.label_steamcmd_path = ctk.CTkLabel(master=self.optionsframe, text="Enter SteamCMD path:") - self.label_steamcmd_path.grid(row=5, column=1, padx=20, pady=(0, 0), columnspan=3, sticky="wn") - - self.edit_steamcmd_path = ctk.CTkEntry(master=self.optionsframe, placeholder_text="Enter your SteamCMD path") - self.edit_steamcmd_path.grid(row=6, column=1, padx=20, pady=(0, 30), columnspan=4, sticky="ewn") - - self.button_steamcmd_browse = ctk.CTkButton(master=self.optionsframe, text="Select", command=self.open_steamcmd_path_browser) - self.button_steamcmd_browse.grid(row=6, column=5, padx=(0, 20), pady=(0, 30), sticky="ewn") - + self.info_button.grid(row=2, column=3, padx=(5, 10), pady=(0, 10), sticky="wn") # set default values self.active_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="T7xwd_theme.json")), "button_active_state_color") self.normal_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="T7xwd_theme.json")), "button_normal_state_color") self.progress_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="T7xwd_theme.json")), "progress_bar_fill_color") self.settings_tab.appearance_mode_optionemenu.set("Dark") - self.settings_tab.scaling_optionemenu.set("100%") + self.settings_tab.scaling_optionemenu.set("80%") self.progress_bar.set(0.0) self.progress_bar.configure(progress_color=self.progress_color) self.hide_settings_widgets() self.button_stop.configure(state="disabled") self.is_pressed = False - self.queue_enabled = False self.queue_stop_button = False self.is_downloading = False + self.is_steamcmd_updating = False self.item_skipped = False + self.steam_updater_size = "Unknown size" self.fail_threshold = 0 - # sidebar windows bouttons + # sidebar windows buttons self.sidebar_main.configure(command=self.main_button_event, text="Main ⬇️", fg_color=(self.active_color), state="active") self.sidebar_library.configure(text="Library 📙", command=self.library_button_event) - self.sidebar_queue.configure(text="Queue 🚧", command=self.queue_button_event) + if self.queue_tab_enabled: self.sidebar_queue.configure(text="Queue 🚧", command=self.queue_button_event) sidebar_settings_button_image = os.path.join(RESOURCES_DIR, "sett10.png") self.sidebar_settings.configure(command=self.settings_button_event, text="", image=ctk.CTkImage(Image.open(sidebar_settings_button_image), size=(int(35), int(35))), fg_color="transparent", width=45, height=45) self.sidebar_settings_tooltip = CTkToolTip(self.sidebar_settings, message="Settings") self.sidebar_library_tooltip = CTkToolTip(self.sidebar_library, message="Experimental") - self.sidebar_queue_tooltip = CTkToolTip(self.sidebar_queue, message="Experimental") + if self.queue_tab_enabled: self.sidebar_queue_tooltip = CTkToolTip(self.sidebar_queue, message="Experimental") self.bind("", lambda e: self.save_window_size_position()) # context_menus self.create_context_menu(self.edit_workshop_id) - self.create_context_menu(self.edit_destination_folder) - self.create_context_menu(self.edit_steamcmd_path) + self.create_context_menu(self.settings_tab.edit_destination_folder) + self.create_context_menu(self.settings_tab.edit_steamcmd_path) self.create_context_menu(self.queuetextarea, textbox=True) self.create_context_menu(self.library_tab.filter_entry, textbox=False, library=True) # valid event required for filter_items() @@ -210,9 +196,10 @@ class T7xWD(ctk.CTk): # load ui configs self.load_configs() - if check_config("checkforupdtes") == "on": + if check_config("checkforupdates") == "on": self.withdraw() - check_for_updates_func(self, ignore_up_todate=True) + try: check_for_updates_func(self, ignore_up_todate=True) + except: pass self.update() self.deiconify() @@ -331,12 +318,16 @@ class T7xWD(ctk.CTk): print("Invalid geometry format:", geometry) def on_closing(self): - save_config("DestinationFolder" ,self.edit_destination_folder.get()) - save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) + save_config("DestinationFolder",self.settings_tab.edit_destination_folder.get()) + save_config("SteamCMDPath",self.settings_tab.edit_steamcmd_path.get()) + startupexe = str(self.settings_tab.edit_startup_exe.get()) + launchargs = str(self.settings_tab.edit_launch_args.get()) + save_config("GameExecutable",startupexe if not isNullOrWhiteSpace(startupexe) else "BlackOps3") + save_config("LaunchParameters",launchargs if not isNullOrWhiteSpace(launchargs) else " ") self.stop_download(on_close=True) os._exit(0) - def id_chnaged_handler(self, some=None, other=None ,shit=None): + def id_changed_handler(self, some=None, other=None ,shit=None): self.after(1, self.label_file_size.configure(text=f"File size: 0KB")) def check_for_updates(self): @@ -349,16 +340,22 @@ class T7xWD(ctk.CTk): def change_scaling_event(self, new_scaling: str): new_scaling_float = int(new_scaling.replace("%", "")) / 100 ctk.set_widget_scaling(new_scaling_float) + ctk.set_window_scaling(new_scaling_float+0.25) save_config("scaling", str(new_scaling_float)) def hide_main_widgets(self): self.optionsframe.grid_forget() self.slider_progressbar_frame.grid_forget() + self.hide_queue_widgets() + + def use_queue_download(self): + return not self.queuetextarea.get("1.0", "end").isspace() def show_main_widgets(self): self.title("T7x Workshop Downloader - Main") self.slider_progressbar_frame.grid(row=2, column=1, rowspan=1, padx=(0, 20), pady=(20, 20), sticky="nsew") - self.optionsframe.grid(row=0, column=1, rowspan=2, padx=(0, 20), pady=(20, 0), sticky="nsew") + self.optionsframe.grid(row=0, column=1, rowspan=1, padx=(0, 20), pady=(20, 0), sticky="nsew") + self.queueframe.grid(row=1, column=1, rowspan=1, padx=(0, 20), pady=(20, 0), sticky="nsew") def hide_settings_widgets(self): self.settings_tab.grid_forget() @@ -373,26 +370,24 @@ class T7xWD(ctk.CTk): def show_library_widgets(self): self.title("T7x Workshop Downloader - Library ➜ Loading... ⏳") - status = self.library_tab.load_items(self.edit_destination_folder.get()) + status = self.library_tab.load_items(self.settings_tab.edit_destination_folder.get()) self.library_tab.grid(row=0, rowspan=3, column=1, padx=(0, 20), pady=(20, 20), sticky="nsew") self.title(f"T7x Workshop Downloader - Library ➜ {status}") def show_queue_widgets(self): self.title("T7x Workshop Downloader - Queue") self.optionsframe.grid_forget() - self.queue_enabled = True self.slider_progressbar_frame.grid(row=2, column=1, rowspan=1, padx=(0, 20), pady=(20, 20), sticky="nsew") - self.qeueuframe.grid(row=0, column=1, rowspan=2, padx=(0, 20), pady=(20, 0), sticky="nsew") + self.queueframe.grid(row=0, column=1, rowspan=2, padx=(0, 20), pady=(20, 0), sticky="nsew") def hide_queue_widgets(self): - self.queue_enabled = False - self.qeueuframe.grid_forget() + self.queueframe.grid_forget() def main_button_event(self): self.sidebar_main.configure(state="active", fg_color=(self.active_color)) self.sidebar_settings.configure(state="normal", fg_color="transparent") self.sidebar_library.configure(state="normal", fg_color=(self.normal_color)) - self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color)) + if self.queue_tab_enabled: self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color)) self.hide_settings_widgets() self.hide_library_widgets() self.hide_queue_widgets() @@ -401,7 +396,7 @@ class T7xWD(ctk.CTk): def settings_button_event(self): self.sidebar_main.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_library.configure(state="normal", fg_color=(self.normal_color)) - self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color)) + if self.queue_tab_enabled: self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_settings.configure(state="active", fg_color=(self.active_color)) self.hide_main_widgets() self.hide_library_widgets() @@ -411,7 +406,7 @@ class T7xWD(ctk.CTk): def library_button_event(self): self.sidebar_main.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_settings.configure(state="normal", fg_color="transparent") - self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color)) + if self.queue_tab_enabled: self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_library.configure(state="active", fg_color=(self.active_color)) self.hide_main_widgets() self.hide_settings_widgets() @@ -422,7 +417,7 @@ class T7xWD(ctk.CTk): self.sidebar_main.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_settings.configure(state="normal", fg_color="transparent") self.sidebar_library.configure(state="normal", fg_color=(self.normal_color)) - self.sidebar_queue.configure(state="active", fg_color=(self.active_color)) + if self.queue_tab_enabled: self.sidebar_queue.configure(state="active", fg_color=(self.active_color)) self.hide_settings_widgets() self.hide_library_widgets() self.show_queue_widgets() @@ -431,12 +426,19 @@ class T7xWD(ctk.CTk): if os.path.exists(CONFIG_FILE_PATH): destination_folder = check_config("DestinationFolder", "") steamcmd_path = check_config("SteamCMDPath", APPLICATION_PATH) + startup_exe = check_config("GameExecutable", "BlackOps3") + launch_params = check_config("LaunchParameters", None) new_appearance_mode = check_config("appearance", "Dark") new_scaling = check_config("scaling", 1.0) - self.edit_destination_folder.delete(0, "end") - self.edit_destination_folder.insert(0, destination_folder) - self.edit_steamcmd_path.delete(0, "end") - self.edit_steamcmd_path.insert(0, steamcmd_path) + self.settings_tab.edit_destination_folder.delete(0, "end") + self.settings_tab.edit_destination_folder.insert(0, destination_folder) + self.settings_tab.edit_steamcmd_path.delete(0, "end") + self.settings_tab.edit_steamcmd_path.insert(0, steamcmd_path) + self.settings_tab.edit_startup_exe.delete(0, "end") + self.settings_tab.edit_startup_exe.insert(0, startup_exe) + if not isNullOrWhiteSpace(launch_params): + self.settings_tab.edit_launch_args.delete(0, "end") + self.settings_tab.edit_launch_args.insert(0, launch_params) ctk.set_appearance_mode(new_appearance_mode) ctk.set_widget_scaling(float(new_scaling)) self.settings_tab.appearance_mode_optionemenu.set(new_appearance_mode) @@ -452,18 +454,20 @@ class T7xWD(ctk.CTk): scaling_float = float(new_scaling)*100 scaling_int = math.trunc(scaling_float) self.settings_tab.scaling_optionemenu.set(f"{scaling_int}%") - self.edit_steamcmd_path.delete(0, "end") - self.edit_steamcmd_path.insert(0, APPLICATION_PATH) + self.settings_tab.edit_steamcmd_path.delete(0, "end") + self.settings_tab.edit_steamcmd_path.insert(0, APPLICATION_PATH) + self.settings_tab.edit_startup_exe.delete(0, "end") + self.settings_tab.edit_startup_exe.insert(0, "BlackOps3") create_default_config() def help_queue_text_func(self, event=None): textarea_content = self.queuetextarea.get("1.0", "end").strip() - help_text = "3010399939,2976006537,2118338989,...\nor:\n3010399939\n2976006537\n2113146805\n..." + help_text = "3010399939,2976006537,2118338989 \n\nor:\n\n3010399939\n2976006537\n2113146805" if any(char.strip() for char in textarea_content): if help_text in textarea_content: - self.workshop_queue_label.configure(text="Workshop IDs/Links => press help to see examples:") + self.workshop_queue_label.configure(text="Batch Downloader:") self.help_button.configure(text="Help") - self.queuetextarea.configure(state="normal") + self.queuetextarea.configure(state="normal",text_color="#fcfcfc") self.queuetextarea.delete(1.0, "end") self.queuetextarea.insert(1.0, "") if self.help_restore_content: @@ -472,37 +476,37 @@ class T7xWD(ctk.CTk): self.queuetextarea.insert(1.0, "") else: if not help_text in textarea_content: - self.help_restore_content = textarea_content - self.workshop_queue_label.configure(text="Workshop IDs/Links => press help to see examples:") + self.help_restore_content = textarea_content + self.workshop_queue_label.configure(text="Batch Downloader:") self.help_button.configure(text="Restore") - self.queuetextarea.configure(state="normal") + self.queuetextarea.configure(state="normal",text_color="#fcfcfc") self.queuetextarea.delete(1.0, "end") self.queuetextarea.insert(1.0, "") - self.workshop_queue_label.configure(text="Workshop IDs/Links => press restore to remove examples:") + self.workshop_queue_label.configure(text="Batch Downloader Example:") self.queuetextarea.insert(1.0, help_text) - self.queuetextarea.configure(state="disabled") + self.queuetextarea.configure(state="disabled", text_color="gray74") else: self.help_restore_content = textarea_content - self.workshop_queue_label.configure(text="Workshop IDs/Links => press restore to remove examples:") + self.workshop_queue_label.configure(text="Batch Downloader Example:") self.help_button.configure(text="Restore") self.queuetextarea.insert(1.0, help_text) - self.queuetextarea.configure(state="disabled") + self.queuetextarea.configure(state="disabled", text_color="gray74") def open_T7x_browser(self): - selected_folder = ctk.filedialog.askdirectory(title="Select T7x Folder") + selected_folder = ctk.filedialog.askdirectory(title="Select Game Folder") if selected_folder: - self.edit_destination_folder.delete(0, "end") - self.edit_destination_folder.insert(0, selected_folder) - save_config("DestinationFolder" ,self.edit_destination_folder.get()) - save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) + self.settings_tab.edit_destination_folder.delete(0, "end") + self.settings_tab.edit_destination_folder.insert(0, selected_folder) + save_config("DestinationFolder" ,self.settings_tab.edit_destination_folder.get()) + save_config("SteamCMDPath" ,self.settings_tab.edit_steamcmd_path.get()) def open_steamcmd_path_browser(self): selected_folder = ctk.filedialog.askdirectory(title="Select SteamCMD Folder") if selected_folder: - self.edit_steamcmd_path.delete(0, "end") - self.edit_steamcmd_path.insert(0, selected_folder) - save_config("DestinationFolder" ,self.edit_destination_folder.get()) - save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) + self.settings_tab.edit_steamcmd_path.delete(0, "end") + self.settings_tab.edit_steamcmd_path.insert(0, selected_folder) + save_config("DestinationFolder" ,self.settings_tab.edit_destination_folder.get()) + save_config("SteamCMDPath" ,self.settings_tab.edit_steamcmd_path.get()) def show_steam_warning_message(self): def callback(): @@ -519,12 +523,15 @@ class T7xWD(ctk.CTk): link = "https://steamcommunity.com/app/311210/workshop/" webbrowser.open(link) + def settings_launch_game(self): + launch_game_func(check_config("destinationfolder"), self.settings_tab.edit_startup_exe.get(), self.settings_tab.edit_launch_args.get()) + @if_internet_available def download_steamcmd(self): - self.edit_steamcmd_path.delete(0, "end") - self.edit_steamcmd_path.insert(0, APPLICATION_PATH) - save_config("DestinationFolder" ,self.edit_destination_folder.get()) - save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) + self.settings_tab.edit_steamcmd_path.delete(0, "end") + self.settings_tab.edit_steamcmd_path.insert(0, APPLICATION_PATH) + save_config("DestinationFolder" ,self.settings_tab.edit_destination_folder.get()) + save_config("SteamCMDPath" ,self.settings_tab.edit_steamcmd_path.get()) steamcmd_url = "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip" steamcmd_zip_path = os.path.join(APPLICATION_PATH, "steamcmd.zip") @@ -795,6 +802,61 @@ class T7xWD(ctk.CTk): self.after(0, main_thread) + def check_if_steamcmd_updating(self, log_file_path): + temp_file_path = log_file_path + '.temp' + if not os.path.exists(log_file_path): + if os.path.isdir(log_file_path): + try: os.makedirs(log_file_path) + except: return False + else: + try: + with open(log_file_path, 'w') as file: + file.write('') + except: pass + + try: shutil.copy2(log_file_path, temp_file_path) + except: return False + + try: + with open(temp_file_path, 'r') as log_file: + log_file.seek(0, os.SEEK_END) + file_size = log_file.tell() + + position = file_size + lines_found = 0 + + while lines_found < 7 and position > 0: + position -= 1 + log_file.seek(position, os.SEEK_SET) + + char = log_file.read(1) + + if char == '\n': + lines_found += 1 + + lines = log_file.readlines()[-7:] + + for _line in reversed(lines): + line = _line.lower().strip() + if "downloading update" in line: + print(line) + match = re.search(r'(\d{1,3}(?:,\d{3})* of \d{1,3}(?:,\d{3})* [kKMGTP]{0,1}B)', _line) + if match: + update_size_str = match.group(1) + self.steam_updater_size = update_size_str + return True + elif self.is_steamcmd_updating and "downloading update" not in line: + self.steam_updater_size = "Installing" + return False + except: + try: + os.remove(temp_file_path) + except: + return False + finally: + try: os.remove(temp_file_path) + except: pass + def check_steamcmd_stdout(self, log_file_path, target_item_id): temp_file_path = log_file_path + '.temp' if not os.path.exists(log_file_path): @@ -846,7 +908,7 @@ class T7xWD(ctk.CTk): def skip_current_queue_item(self): if self.button_download._state == "normal": - self.skip_boutton.grid_remove() + self.skip_button.grid_remove() self.after(1, self.status_text.configure(text=f"Status: Standby!")) return self.settings_tab.stopped = True @@ -858,7 +920,7 @@ class T7xWD(ctk.CTk): subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW) - self.skip_boutton.grid_remove() + self.skip_button.grid_remove() self.after(2, self.status_text.configure(text=f"Status: Skipping...")) self.label_speed.configure(text="Network Speed: 0 KB/s") self.progress_text.configure(text="0%") @@ -867,6 +929,7 @@ class T7xWD(ctk.CTk): # the real deal def run_steamcmd_command(self, command, map_folder, wsid, queue=None): steamcmd_path = get_steamcmd_path() + steamcmd_bootstrap_logs = os.path.join(steamcmd_path, "logs", "bootstrap_log.txt") stdout_path = os.path.join(steamcmd_path, "logs", "workshop_log.txt") timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") @@ -874,9 +937,13 @@ class T7xWD(ctk.CTk): except: pass try: + with open(steamcmd_bootstrap_logs, 'w') as file: + file.write('') with open(stdout_path, 'w') as file: file.write('') except: + try: os.rename(stdout_path, os.path.join(map_folder, os.path.join(steamcmd_bootstrap_logs, f"bootstrap_log_couldntremove_{timestamp}.txt"))) + except: pass try: os.rename(stdout_path, os.path.join(map_folder, os.path.join(stdout_path, f"workshop_log_couldntremove_{timestamp}.txt"))) except: pass @@ -918,14 +985,18 @@ class T7xWD(ctk.CTk): if process.poll() is not None: break if not self.is_downloading: + if self.check_if_steamcmd_updating(steamcmd_bootstrap_logs): + self.is_steamcmd_updating = True if self.check_steamcmd_stdout(stdout_path, wsid): start_time = time.time() self.is_downloading = True + self.is_steamcmd_updating = False elapsed_time = time.time() - start_time time.sleep(1) # print("Broken freeeee!") self.is_downloading = False + self.is_steamcmd_updating = False try: with open(stdout_path, 'w') as file: file.write('') @@ -968,6 +1039,8 @@ class T7xWD(ctk.CTk): if process.poll() is not None: break if not self.is_downloading: + if self.check_if_steamcmd_updating(steamcmd_bootstrap_logs): + self.is_steamcmd_updating = True if self.check_steamcmd_stdout(stdout_path, wsid): start_time = time.time() self.is_downloading = True @@ -976,6 +1049,7 @@ class T7xWD(ctk.CTk): # print("Broken freeeee!") self.is_downloading = False + self.is_steamcmd_updating = False try: with open(stdout_path, 'w') as file: file.write('') @@ -1011,7 +1085,7 @@ class T7xWD(ctk.CTk): msg = CTkMessagebox(title="Downloads Complete", message=message, icon="info", option_1="Launch", option_2="Ok", sound=True) response = msg.get() if response=="Launch": - launch_T7x_func(self.edit_destination_folder.get().strip()) + launch_game_func(self.settings_tab.edit_destination_folder.get().strip(), self.settings_tab.edit_startup_exe.get(), self.settings_tab.edit_launch_args.get()) if response=="Ok": return self.after(0, callback) @@ -1023,8 +1097,8 @@ class T7xWD(ctk.CTk): if not self.is_pressed: self.after(1, self.label_speed.configure(text=f"Loading...")) self.is_pressed = True - self.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) - if self.queue_enabled: + self.library_tab.load_items(self.settings_tab.edit_destination_folder.get(), dont_add=True) + if self.use_queue_download(): self.item_skipped = False start_down_thread = threading.Thread(target=self.queue_download_thread, args=(update,)) start_down_thread.start() @@ -1038,8 +1112,8 @@ class T7xWD(ctk.CTk): self.stopped = False self.queue_stop_button = False try: - save_config("DestinationFolder" ,self.edit_destination_folder.get()) - save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) + save_config("DestinationFolder" ,self.settings_tab.edit_destination_folder.get()) + save_config("SteamCMDPath" ,self.settings_tab.edit_steamcmd_path.get()) if not check_steamcmd(): self.show_steam_warning_message() @@ -1065,7 +1139,7 @@ class T7xWD(ctk.CTk): self.stop_download() return - destination_folder = self.edit_destination_folder.get().strip() + destination_folder = self.settings_tab.edit_destination_folder.get().strip() if not destination_folder or not os.path.exists(destination_folder): show_message("Error", "Please select a valid destination folder => in the main tab!.") @@ -1102,13 +1176,16 @@ class T7xWD(ctk.CTk): ws_file_size = get_workshop_file_size(workshop_id) file_size = ws_file_size - items_ws_sizes[workshop_id] = ws_file_size - self.total_queue_size += ws_file_size if file_size is None: - show_message("Error", "Failed to retrieve file size.", icon="cancel") - self.stop_download() - return + ws_file_size = 1 + file_size = 1 + show_message("Error", "Failed to retrieve file size, Continuing anyway", icon="cancel") + # self.stop_download() + # return + + items_ws_sizes[workshop_id] = ws_file_size + self.total_queue_size += ws_file_size if any(workshop_id in item for item in self.library_tab.added_items): self.already_installed.append(workshop_id) @@ -1160,7 +1237,7 @@ class T7xWD(ctk.CTk): self.stop_download() return ws_file_size = get_workshop_file_size(workshop_id) - file_size = ws_file_size + file_size = ws_file_size if ws_file_size is not None else 1 self.after(1, lambda mid=workshop_id: self.label_file_size.configure(text=f"File size: {get_workshop_file_size(mid ,raw=True)}")) download_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "downloads", "311210", workshop_id) map_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", workshop_id) @@ -1170,7 +1247,7 @@ class T7xWD(ctk.CTk): def check_and_update_progress(): previous_net_speed = 0 est_downloaded_bytes = 0 - file_size = ws_file_size + file_size = ws_file_size if ws_file_size is not None else 1 item_name = get_item_name(workshop_id) if get_item_name(workshop_id) else "Error getting name" while not self.settings_tab.stopped: @@ -1196,7 +1273,10 @@ class T7xWD(ctk.CTk): self.item_skipped = False while not self.is_downloading and not self.settings_tab.stopped: - self.after(1, self.label_speed.configure(text=f"Waiting for steamcmd...")) + if self.is_steamcmd_updating: + self.after(1, self.label_speed.configure(text=f"Updating steamcmd ({self.steam_updater_size})...")) + else: + self.after(1, self.label_speed.configure(text=f"Waiting for steamcmd...")) time_elapsed = time.time() - start_time elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed) if self.settings_tab.show_fails: @@ -1206,11 +1286,12 @@ class T7xWD(ctk.CTk): self.after(1, self.status_text.configure( text=f"Status: Total size: ~{convert_bytes_to_readable(self.total_queue_size)} | ID: {workshop_id} | {item_name} | Waiting {current_number}/{total_items}")) if len(items) > 1: - self.skip_boutton.grid(row=3, column=1, padx=(10, 20), pady=(0, 25), sticky="ws") + self.skip_button.grid(row=1, column=3, padx=(0, 20), pady=(0, 8), sticky="es") if index == len(items) - 1: - self.skip_boutton.grid_remove() + self.skip_button.grid_remove() time.sleep(1) if self.is_downloading: + self.is_steamcmd_updating = False break try: @@ -1355,7 +1436,7 @@ class T7xWD(ctk.CTk): remove_tree(map_folder) remove_tree(download_folder) - self.library_tab.update_item(self.edit_destination_folder.get(), workshop_id, mod_type, folder_name) + self.library_tab.update_item(self.settings_tab.edit_destination_folder.get(), workshop_id, mod_type, folder_name) if index == len(items) - 1: self.after(1, self.status_text.configure(text=f"Status: Done! => Please press stop only if you see no popup window (rare bug)")) @@ -1378,7 +1459,7 @@ class T7xWD(ctk.CTk): self.button_download.configure(state="normal") self.button_stop.configure(state="disabled") self.after(1, self.status_text.configure(text=f"Status: Done!")) - self.skip_boutton.grid_remove() + self.skip_button.grid_remove() self.after(1, self.label_file_size.configure(text=f"File size: 0KB")) self.settings_tab.stopped = True self.stop_download() @@ -1393,8 +1474,8 @@ class T7xWD(ctk.CTk): try: self.settings_tab.stopped = False - save_config("DestinationFolder" ,self.edit_destination_folder.get()) - save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) + save_config("DestinationFolder" ,self.settings_tab.edit_destination_folder.get()) + save_config("SteamCMDPath" ,self.settings_tab.edit_steamcmd_path.get()) if not check_steamcmd(): self.show_steam_warning_message() @@ -1408,7 +1489,7 @@ class T7xWD(ctk.CTk): workshop_id = self.edit_workshop_id.get().strip() - destination_folder = self.edit_destination_folder.get().strip() + destination_folder = self.settings_tab.edit_destination_folder.get().strip() if not destination_folder or not os.path.exists(destination_folder): show_message("Error", "Please select a valid destination folder.") @@ -1437,14 +1518,16 @@ class T7xWD(ctk.CTk): file_size = ws_file_size if not valid_id(workshop_id): - show_message("Warning", "Please enter a valid Workshop ID/Link.", icon="warning") + show_message("Warning", "Invalid Workshop ID/Link.\nYou can attempt to update with 'Skip Invalid Items' disabled.", icon="warning") self.stop_download() return if file_size is None: - show_message("Error", "Failed to retrieve file size.", icon="cancel") - self.stop_download() - return + ws_file_size = 1 + file_size = 1 + show_message("Error", "Failed to retrieve file size, Continuing anyway", icon="cancel") + # self.stop_download() + # return if not update: if any(workshop_id in item for item in self.library_tab.added_items): @@ -1464,7 +1547,7 @@ class T7xWD(ctk.CTk): previous_net_speed = 0 est_downloaded_bytes = 0 start_time = time.time() - file_size = ws_file_size + file_size = ws_file_size if ws_file_size is not None else 1 while not self.settings_tab.stopped: if self.settings_tab.steamcmd_reset: @@ -1473,7 +1556,10 @@ class T7xWD(ctk.CTk): est_downloaded_bytes = 0 while not self.is_downloading and not self.settings_tab.stopped: - self.after(1, self.label_speed.configure(text=f"Waiting for steamcmd...")) + if self.is_steamcmd_updating: + self.after(1, self.label_speed.configure(text=f"Updating steamcmd ({self.steam_updater_size})...")) + else: + self.after(1, self.label_speed.configure(text=f"Waiting for steamcmd...")) time_elapsed = time.time() - start_time elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed) if self.settings_tab.show_fails: @@ -1482,6 +1568,7 @@ class T7xWD(ctk.CTk): self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d}")) time.sleep(1) if self.is_downloading: + self.is_steamcmd_updating = False break try: @@ -1626,7 +1713,7 @@ class T7xWD(ctk.CTk): remove_tree(download_folder) if not invalid_item_folder: - self.library_tab.update_item(self.edit_destination_folder.get(), workshop_id, mod_type, folder_name) + self.library_tab.update_item(self.settings_tab.edit_destination_folder.get(), workshop_id, mod_type, folder_name) self.show_complete_message(message=f"{mod_type.capitalize()} files were downloaded\nYou can run the game now!\nPS: You have to restart the game \n(pressing launch will launch/restarts)") self.button_download.configure(state="normal") self.button_stop.configure(state="disabled") @@ -1692,4 +1779,4 @@ class T7xWD(ctk.CTk): self.progress_bar.set(0.0) self.after(50, self.status_text.configure(text=f"Status: Standby!")) self.after(1, self.label_speed.configure(text=f"Awaiting Download!")) - self.skip_boutton.grid_remove() + self.skip_button.grid_remove() diff --git a/t7xwd_package/src/settings_tab.py b/t7xwd_package/src/settings_tab.py index 50b0031..370045c 100644 --- a/t7xwd_package/src/settings_tab.py +++ b/t7xwd_package/src/settings_tab.py @@ -28,12 +28,16 @@ class SettingsTab(ctk.CTkFrame): self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) left_frame = ctk.CTkFrame(self) - left_frame.grid(row=0, column=0, padx=(20, 20), pady=(20, 0), sticky="nsew") + left_frame.grid(row=0, column=0, columnspan=2, padx=(20, 20), pady=(20, 0), sticky="nsew") left_frame.grid_columnconfigure(1, weight=1) right_frame = ctk.CTkFrame(self) - right_frame.grid(row=0, column=1, padx=(20, 20), pady=(20, 0), sticky="nsew") + right_frame.grid(row=0, column=2, padx=(0, 20), pady=(20, 0), sticky="nsew") right_frame.grid_columnconfigure(1, weight=1) self.update_idletasks() + + # Save button + self.save_button = ctk.CTkButton(self, text="Save", command=self.save_settings, state='disabled') + self.save_button.grid(row=3, column=1, padx=40, pady=(20, 20), sticky="ne") # Check for updates checkbox self.check_updates_var = ctk.BooleanVar() @@ -45,7 +49,7 @@ class SettingsTab(ctk.CTkFrame): # Show console checkbox self.console_var = ctk.BooleanVar() self.console_var.trace_add("write", self.enable_save_button) - self.checkbox_show_console = ctk.CTkSwitch(left_frame, text="Console (On Download)", variable=self.console_var) + self.checkbox_show_console = ctk.CTkSwitch(left_frame, text="Display SteamCMD console", variable=self.console_var) self.checkbox_show_console.grid(row=1, column=1, padx=20, pady=(20, 0), sticky="nw") self.checkbox_show_console_tooltip = CTkToolTip(self.checkbox_show_console, message="Toggle SteamCMD console\nPlease don't close the Console If you want to stop press the Stop button") self.console_var.set(self.load_settings("console")) @@ -53,7 +57,7 @@ class SettingsTab(ctk.CTkFrame): # Show continuous checkbox self.continuous_var = ctk.BooleanVar() self.continuous_var.trace_add("write", self.enable_save_button) - self.checkbox_continuous = ctk.CTkSwitch(left_frame, text="Continuous Download", variable=self.continuous_var) + self.checkbox_continuous = ctk.CTkSwitch(left_frame, text="Continuous download", variable=self.continuous_var) self.checkbox_continuous.grid(row=2, column=1, padx=20, pady=(20, 0), sticky="nw") self.checkbox_continuous_tooltip = CTkToolTip(self.checkbox_continuous, message="This will make sure that the download restarts and resumes! until it finishes if steamcmd crashes randomly (it will not redownload from the start)") self.continuous_var.set(self.load_settings("continuous_download")) @@ -69,16 +73,16 @@ class SettingsTab(ctk.CTkFrame): # Show estimated_progress checkbox self.estimated_progress_var = ctk.BooleanVar() self.estimated_progress_var.trace_add("write", self.enable_save_button) - self.estimated_progress_cb = ctk.CTkSwitch(left_frame, text="Estimated Progress Bar", variable=self.estimated_progress_var) + self.estimated_progress_cb = ctk.CTkSwitch(left_frame, text="Estimated progress bar", variable=self.estimated_progress_var) self.estimated_progress_cb.grid(row=4, column=1, padx=20, pady=(20, 0), sticky="nw") self.estimated_progress_var_tooltip = CTkToolTip(self.estimated_progress_cb, message="This will change how to progress bar works by estimating how long the download will take\ \nThis is not accurate ,it's better than with it off which is calculating the downloaded folder size which steamcmd dumps the full size rigth mostly") self.estimated_progress_var.set(self.load_settings("estimated_progress", "on")) - # Show show fails checkbox + # Show fails checkbox self.show_fails_var = ctk.BooleanVar() self.show_fails_var.trace_add("write", self.enable_save_button) - self.show_fails_cb = ctk.CTkSwitch(left_frame, text="Show fails (on top of progress bar)", variable=self.show_fails_var) + self.show_fails_cb = ctk.CTkSwitch(left_frame, text="Show fails", variable=self.show_fails_var) self.show_fails_cb.grid(row=5, column=1, padx=20, pady=(20, 0), sticky="nw") self.show_fails_tooltip = CTkToolTip(self.show_fails_cb, message="Display how many times steamcmd has failed/crashed\nIf the number is getting high quickly then try pressing Reset SteamCMD and try again, otherwise its fine") self.estimated_progress_var.set(self.load_settings("show_fails", "on")) @@ -94,75 +98,138 @@ class SettingsTab(ctk.CTkFrame): # check items for update on launch self.check_items_var = ctk.BooleanVar() self.check_items_var.trace_add("write", self.enable_save_button) - self.check_items_ch = ctk.CTkSwitch(left_frame, text="Check Library items on launch", variable=self.check_items_var) + self.check_items_ch = ctk.CTkSwitch(left_frame, text="Check library items on launch", variable=self.check_items_var) self.check_items_ch.grid(row=7, column=1, padx=20, pady=(20, 0), sticky="nw") self.check_items_tooltip = CTkToolTip(self.check_items_ch, message="This will show a window on launch of items that have pending updates -> you can open it manually from library tab") self.check_items_var.set(self.load_settings("check_items", "off")) + + + # TODO: get windows size to padx for the following checkboxes + + # update invalid + self.invalid_items_var = ctk.BooleanVar() + self.invalid_items_var.trace_add("write", self.enable_save_button) + self.invalid_items_ch = ctk.CTkSwitch(left_frame, text="Update invalid items", variable=self.invalid_items_var) + self.invalid_items_ch.grid(row=0, column=1, padx=(300,0), pady=(20, 0), sticky="nw") + self.invalid_items_tooltip = CTkToolTip(self.invalid_items_ch, message="Allow updating invalid items from the details tab") + self.invalid_items_var.set(self.load_settings("update_invalid", "off")) - # Resetr steam on many fails + # skip invalid + self.skip_items_var = ctk.BooleanVar() + self.skip_items_var.trace_add("write", self.enable_save_button) + self.skip_items_ch = ctk.CTkSwitch(left_frame, text="Skip invalid items", variable=self.skip_items_var) + self.skip_items_ch.grid(row=1, column=1, padx=(300,0), pady=(20, 0), sticky="nw") + self.skip_items_tooltip = CTkToolTip(self.skip_items_ch, message="Skip invalid items") + self.skip_items_var.set(self.load_settings("skip_invalid", "off")) + + # text input fields + self.label_destination_folder = ctk.CTkLabel(left_frame, text='Enter Game folder:') + self.label_destination_folder.grid(row=8, column=1, padx=20, pady=(20, 0), columnspan=1, sticky="ws") + + self.entry_var1 = ctk.StringVar(value="") + self.edit_destination_folder = ctk.CTkEntry(left_frame, placeholder_text="game installation folder", textvariable=self.entry_var1) + self.edit_destination_folder.grid(row=9, column=1, padx=20, pady=(0, 10), columnspan=1, sticky="ewn") + self.entry_var1.trace_add("write", self.enable_save_button) + + self.button_T7x_browse = ctk.CTkButton(left_frame, text="Select", command=self.open_T7x_browser) + self.button_T7x_browse.grid(row=9, column=2, padx=(0, 20), pady=(0, 10), sticky="ewn") + + self.label_steamcmd_path = ctk.CTkLabel(left_frame, text="Enter SteamCMD path:") + self.label_steamcmd_path.grid(row=10, column=1, padx=20, pady=(0, 0), columnspan=1, sticky="wn") + + self.entry_var2 = ctk.StringVar(value="") + self.edit_steamcmd_path = ctk.CTkEntry(left_frame, placeholder_text="Enter SteamCMD path", textvariable=self.entry_var2) + self.edit_steamcmd_path.grid(row=11, column=1, padx=20, pady=(0, 10), columnspan=1, sticky="ewn") + self.entry_var2.trace_add("write", self.enable_save_button) + + self.button_steamcmd_browse = ctk.CTkButton(left_frame, text="Select", command=self.open_steamcmd_path_browser) + self.button_steamcmd_browse.grid(row=11, column=2, padx=(0, 20), pady=(0, 10), sticky="ewn") + + self.label_launch_args = ctk.CTkLabel(left_frame, text='Launch Parameters:') + self.label_launch_args.grid(row=12, column=1, padx=20, pady=(0, 0), columnspan=1, sticky="ws") + + self.edit_startup_exe = ctk.CTkEntry(left_frame, placeholder_text="exe") + self.edit_startup_exe.grid(row=13, column=1, padx=(20,0), pady=(0, 20), columnspan=1, sticky="we") + + self.edit_launch_args = ctk.CTkEntry(left_frame, placeholder_text="launch arguments") + self.edit_launch_args.grid(row=13, column=1, padx=(140,20), pady=(0, 20), columnspan=2, sticky="we") + + # Check for updates button n Launch game + self.check_for_updates = ctk.CTkButton(right_frame, text="Check for updates", command=self.settings_check_for_updates) + self.check_for_updates.grid(row=1, column=1, padx=20, pady=(20, 0), sticky="n") + + # self.launch_game = ctk.CTkButton(right_frame, text="Launch game", command=self.settings_launch_game) + # self.launch_game.grid(row=2, column=1, padx=20, pady=(10, 0), sticky="n") + + self.reset_steamcmd = ctk.CTkButton(right_frame, text="Reset SteamCMD", command=self.settings_reset_steamcmd) + self.reset_steamcmd.grid(row=3, column=1, padx=20, pady=(10, 0), sticky="n") + self.reset_steamcmd_tooltip = CTkToolTip(self.reset_steamcmd, message="This will remove steamapps folder + all the maps that are potentioaly corrupted\nor not so use at ur own risk (could fix some issues as well)") + + self.workshop_to_gamedir = ctk.CTkButton(right_frame, text="Workshop Transfer", command=self.workshop_to_gamedir_toplevel) + self.workshop_to_gamedir.grid(row=4, column=1, padx=20, pady=(10, 0), sticky="n") + self.workshop_to_gamedir_tooltip = CTkToolTip(self.workshop_to_gamedir, message="Copy/Move maps and mods from Workshop to the game directory (opens up a window)") + + # appearance + self.appearance_mode_label = ctk.CTkLabel(right_frame, text="Appearance:", anchor="w") + self.appearance_mode_label.grid(row=5, column=1, padx=20, pady=(150, 0), sticky="nw") + self.appearance_mode_optionemenu = ctk.CTkOptionMenu(right_frame, values=["Light", "Dark", "System"], + command=master.change_appearance_mode_event) + self.appearance_mode_optionemenu.grid(row=6, column=1, padx=20, pady=(0, 0), sticky="nw") + self.scaling_label = ctk.CTkLabel(right_frame, text="UI Scaling:", anchor="w") + self.scaling_label.grid(row=7, column=1, padx=20, pady=(0, 0), sticky="nw") + self.scaling_optionemenu = ctk.CTkOptionMenu(right_frame, values=["60%", "70%", "80%", "90%", "100%", "110%"], + command=master.change_scaling_event) + self.scaling_optionemenu.grid(row=8, column=1, padx=20, pady=(0, 0), sticky="nw") + + # self.custom_theme = ctk.CTkButton(right_frame, text="Custom theme", command=self.T7xwd_custom_theme) + # self.custom_theme.grid(row=9, column=1, padx=20, pady=(20, 0), sticky="n") + + self.theme_options_label = ctk.CTkLabel(right_frame, text="Theme:", anchor="w") + self.theme_options_label.grid(row=9, column=1, padx=20, pady=(0, 0), sticky="nw") + self.theme_options = ctk.CTkOptionMenu(right_frame, values=["Default", "Blue", "Grey", "Obsidian", "Ghost","NeonBanana", "Custom"], + command=self.theme_options_func) + self.theme_options.grid(row=10, column=1, padx=20, pady=(0, 0), sticky="nw") + self.theme_options.set(value=self.load_settings("theme", "Default")) + + # Reset steam on many fails self.reset_steamcmd_on_fail_var = ctk.IntVar() self.reset_steamcmd_on_fail_var.trace_add("write", self.enable_save_button) - self.reset_steamcmd_on_fail_text = ctk.CTkLabel(left_frame, text=f"Reset steamcmd: (n of fails):", anchor="w") - self.reset_steamcmd_on_fail_text.grid(row=8, column=1, padx=20, pady=(10, 0), sticky="nw") - self.reset_steamcmd_on_fail = ctk.CTkOptionMenu(left_frame, values=["5", "10", "20", "30", "40", "Custom", "Disable"], variable=self.reset_steamcmd_on_fail_var, command=self.reset_steamcmd_on_fail_func) - self.reset_steamcmd_on_fail.grid(row=8, column=1, padx=(190, 0), pady=(10, 0), sticky="nw") - self.reset_steamcmd_on_fail_tooltip = CTkToolTip(self.reset_steamcmd_on_fail, message="This actually fixes steamcmd when its crashing way too much") + self.reset_steamcmd_on_fail_text = ctk.CTkLabel(right_frame, text=f"Download Attempts:", anchor="w") + self.reset_steamcmd_on_fail_text.grid(row=11, column=1, padx=20, pady=(0, 0), sticky="nw") + self.reset_steamcmd_on_fail = ctk.CTkOptionMenu(right_frame, values=["5", "10", "15", "20", "Custom", "Disable"], variable=self.reset_steamcmd_on_fail_var, command=self.reset_steamcmd_on_fail_func) + self.reset_steamcmd_on_fail.grid(row=12, column=1, padx=20, pady=(0, 0), sticky="nw") + self.reset_steamcmd_on_fail_tooltip = CTkToolTip(self.reset_steamcmd_on_fail, message="Number of failed download attempts before resetting SteamCMD") self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "10")) # item folder naming self.folder_options_label_var = ctk.IntVar() self.folder_options_label_var.trace_add("write", self.enable_save_button) - self.folder_options_label = ctk.CTkLabel(left_frame, text="Items Folder Naming:", anchor="nw") - self.folder_options_label.grid(row=10, column=1, padx=20, pady=(10, 0), sticky="nw") - self.folder_options = ctk.CTkOptionMenu(left_frame, values=["PublisherID", "FolderName"], command=self.change_folder_naming, + self.folder_options_label = ctk.CTkLabel(right_frame, text="Items Folder Naming:", anchor="w") + self.folder_options_label.grid(row=13, column=1, padx=(20,0), pady=(0, 0), sticky="nw") + self.folder_options = ctk.CTkOptionMenu(right_frame, values=["PublisherID", "FolderName"], command=self.change_folder_naming, variable=self.folder_options_label_var) - self.folder_options.grid(row=10, column=1, padx=(150, 0), pady=(3, 0), sticky="nw") + self.folder_options.grid(row=14, column=1, padx=20, pady=(0, 0), sticky="nw") self.folder_options.set(value=self.load_settings("folder_naming", "PublisherID")) - # Check for updates button n Launch T7x - self.check_for_updates = ctk.CTkButton(right_frame, text="Check for updates", command=self.settings_check_for_updates) - self.check_for_updates.grid(row=1, column=1, padx=20, pady=(20, 0), sticky="n") - - self.launch_T7x = ctk.CTkButton(right_frame, text="Launch T7x", command=self.settings_launch_T7x) - self.launch_T7x.grid(row=2, column=1, padx=20, pady=(20, 0), sticky="n") - - self.reset_steamcmd = ctk.CTkButton(right_frame, text="Reset SteamCMD", command=self.settings_reset_steamcmd) - self.reset_steamcmd.grid(row=3, column=1, padx=20, pady=(20, 0), sticky="n") - self.reset_steamcmd_tooltip = CTkToolTip(self.reset_steamcmd, message="This will remove steamapps folder + all the maps that are potentioaly corrupted\nor not so use at ur own risk (could fix some issues as well)") - - self.steam_to_T7x = ctk.CTkButton(right_frame, text="Steam to T7x", command=self.from_steam_to_T7x_toplevel) - self.steam_to_T7x.grid(row=5, column=1, padx=20, pady=(20, 0), sticky="n") - self.steam_to_T7x_tooltip = CTkToolTip(self.steam_to_T7x, message="Moves/copies maps and mods from steam to T7x (opens up a window)") - - # appearance - self.appearance_mode_label = ctk.CTkLabel(right_frame, text="Appearance Mode:", anchor="n") - self.appearance_mode_label.grid(row=6, column=1, padx=20, pady=(20, 0)) - self.appearance_mode_optionemenu = ctk.CTkOptionMenu(right_frame, values=["Light", "Dark", "System"], - command=master.change_appearance_mode_event) - self.appearance_mode_optionemenu.grid(row=7, column=1, padx=20, pady=(0, 0)) - self.scaling_label = ctk.CTkLabel(right_frame, text="UI Scaling:", anchor="n") - self.scaling_label.grid(row=8, column=1, padx=20, pady=(10, 0)) - self.scaling_optionemenu = ctk.CTkOptionMenu(right_frame, values=["80%", "90%", "100%", "110%", "120%"], - command=master.change_scaling_event) - self.scaling_optionemenu.grid(row=9, column=1, padx=20, pady=(0, 0)) - - # self.custom_theme = ctk.CTkButton(right_frame, text="Custom theme", command=self.T7xwd_custom_theme) - # self.custom_theme.grid(row=8, column=1, padx=20, pady=(20, 0), sticky="n") - - self.theme_options_label = ctk.CTkLabel(right_frame, text="Themes:", anchor="n") - self.theme_options_label.grid(row=10, column=1, padx=20, pady=(10, 0)) - self.theme_options = ctk.CTkOptionMenu(right_frame, values=["Default", "Blue", "Grey", "Obsidian", "Ghost","NeonBanana", "Custom"], - command=self.theme_options_func) - self.theme_options.grid(row=11, column=1, padx=20, pady=(0, 0)) - self.theme_options.set(value=self.load_settings("theme", "Default")) - - # Save button - self.save_button = ctk.CTkButton(self, text="Save", command=self.save_settings, state='disabled') - self.save_button.grid(row=3, column=0, padx=20, pady=(20, 20), sticky="nw") - #version self.version_info = ctk.CTkLabel(self, text=f"{VERSION}") - self.version_info.grid(row=3, column=1, padx=20, pady=(20, 20), sticky="e") + self.version_info.grid(row=3, column=2, padx=20, pady=(20, 20), sticky="e") + + def open_T7x_browser(self): + selected_folder = ctk.filedialog.askdirectory(title="Select Game Folder") + if selected_folder: + self.edit_destination_folder.delete(0, "end") + self.edit_destination_folder.insert(0, selected_folder) + save_config("DestinationFolder" ,self.edit_destination_folder.get()) + save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) + + def open_steamcmd_path_browser(self): + selected_folder = ctk.filedialog.askdirectory(title="Select SteamCMD Folder") + if selected_folder: + self.edit_steamcmd_path.delete(0, "end") + self.edit_steamcmd_path.insert(0, selected_folder) + save_config("DestinationFolder" ,self.edit_destination_folder.get()) + save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) def reset_steamcmd_on_fail_func(self, option: str): if option == "Custom": @@ -225,6 +292,9 @@ class SettingsTab(ctk.CTkFrame): def save_settings(self): self.save_button.configure(state='disabled') + save_config("GameExecutable", str(self.edit_startup_exe.get()) if not isNullOrWhiteSpace(self.edit_startup_exe.get()) else "BlackOps3") + save_config("LaunchParameters", str(self.edit_launch_args.get()) if not isNullOrWhiteSpace(self.edit_launch_args.get()) else " ") + if self.folder_options.get() == "PublisherID": save_config("folder_naming", "0") else: @@ -234,11 +304,21 @@ class SettingsTab(ctk.CTkFrame): save_config("check_items", "on") else: save_config("check_items", "off") + + if self.invalid_items_var.get(): + save_config("update_invalid", "on") + else: + save_config("update_invalid", "off") + + if self.skip_items_var.get(): + save_config("skip_invalid", "on") + else: + save_config("skip_invalid", "off") if self.check_updates_checkbox.get(): - save_config("checkforupdtes", "on") + save_config("checkforupdates", "on") else: - save_config("checkforupdtes", "off") + save_config("checkforupdates", "off") if self.checkbox_show_console.get(): save_config("console", "on") @@ -380,8 +460,8 @@ class SettingsTab(ctk.CTkFrame): file_to_rename = os.path.join(APPLICATION_PATH, "T7xwd_theme.json") if os.path.exists(file_to_rename): timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") - new_name = f"T7xwd_theme_{timestamp}.json" - os.rename(file_to_rename, os.path.join(APPLICATION_PATH, new_name)) + name = f"T7xwd_theme_{timestamp}.json" + os.rename(file_to_rename, os.path.join(APPLICATION_PATH, name)) if not disable_only: show_message("Preset file renamed", "Custom preset disabled, file has been renmaed\n* Restart the app to take effect", icon="info") @@ -399,9 +479,9 @@ class SettingsTab(ctk.CTkFrame): # make this rename to {id}_duplicate as a fallback def rename_all_folders(self, option): - T7xFolder = main_app.app.edit_destination_folder.get() - maps_folder = os.path.join(T7xFolder, "mods") - mods_folder = os.path.join(T7xFolder, "usermaps") + gameFolder = self.edit_destination_folder.get() + mods_folder = os.path.join(gameFolder, "mods") + maps_folder = os.path.join(gameFolder, "usermaps") folders_to_process = [] @@ -412,61 +492,105 @@ class SettingsTab(ctk.CTkFrame): folders_to_process.append(maps_folder) if not os.path.exists(maps_folder) and not os.path.exists(mods_folder): - show_message("Warning -> Check T7x path", f"You don't have any items yet ,from now on item's folders will be named as their {option}") + show_message("Warning -> Check game path", + f"You don't have any items yet, from now on item's folders will be named as their {option}") return 0 processed_names = set() - for folder_path in folders_to_process: - for folder_name in os.listdir(folder_path): - zone_path = os.path.join(folder_path, folder_name, "zone") - if not os.path.isdir(zone_path): - continue - if folder_name in main_app.app.library_tab.item_block_list: - continue + files = Path(folders_to_process[0]).glob("*/zone/workshop.json") + items = dict() + ignored_folders = [] - json_path = os.path.join(zone_path, "workshop.json") - publisher_id = extract_json_data(json_path, 'PublisherID') - new_name = extract_json_data(json_path, option) - if folder_name == new_name: - continue + for idx, file in enumerate(files): + curr_folder_name = os.path.relpath(file, folders_to_process[0]).split("\\", 1)[0] - rename_flag = True + with open(file, 'r') as json_file: + data = json.load(json_file) + _item = { + 'PublisherID': data.get('PublisherID'), + 'Name': data.get(option), + 'current_folder': curr_folder_name + } + if _item.get('PublisherID')!="" and int(_item.get('PublisherID'))>0: + items[idx] = _item + else: + ignored_folders.append(curr_folder_name) - if os.path.exists(json_path): - folder_to_rename = os.path.join(folder_path, folder_name) - new_folder_name = new_name - while new_folder_name in processed_names: - if new_name == publisher_id: - new_folder_name += f"_duplicated" + IDs = [x['PublisherID'] for x in items.values()] + Names = [x['Name'] for x in items.values()] + currFolder = [x['current_folder'] for x in items.values()] + + def indices(lst, item): + return [i for i, x in enumerate(lst) if item in x] + + def find_duplicate_items_in_list(list, items): + return dict((x, indices(list, x)) for x in [y for y in items if items.count(y) > 1]) + + def prep_rename(changelist, orig, new): + return changelist.append((os.path.join(folders_to_process[0], orig), os.path.join(folders_to_process[0], new))) + + duplicates = find_duplicate_items_in_list(Names, Names) + duplicates_IDs = find_duplicate_items_in_list(IDs, IDs) + + duplicate_idx = concatenate_sublists(duplicates.values()) + + changelist = [] + + for i in range(len(IDs)): + if i not in duplicate_idx: + prep_rename(changelist, currFolder[i], Names[i]) + + for v in duplicates.values(): + if len(v) == 2: + if IDs[v[0]] == IDs[v[1]]: + prep_rename(changelist, currFolder[v[0]], Names[v[0]]) + prep_rename( + changelist, currFolder[v[1]], Names[v[1]]+"_duplicate") + else: + prep_rename( + changelist, currFolder[v[0]], Names[v[0]]+f"_{IDs[v[0]]}") + prep_rename( + changelist, currFolder[v[1]], Names[v[1]]+f"_{IDs[v[1]]}") + + if len(v) > 2: + for j, i in enumerate(v): + if i in (duplicates_IDs.get(f'{IDs[i]}') if duplicates_IDs.get(f'{IDs[i]}') is not None else []): + if i == v[0]: + if Names[i].startswith(IDs[i]): + prep_rename( + changelist, currFolder[i], Names[i]) + else: + prep_rename( + changelist, currFolder[i], Names[i]+f"_{IDs[i]}") else: - new_folder_name += f"_{publisher_id}" - if folder_name == new_folder_name: - rename_flag = False - break - new_path = os.path.join(folder_path, new_folder_name) + if Names[i].startswith(IDs[i]): + newname = Names[i]+f"_duplicate" + else: + newname = Names[i]+f"_{IDs[i]}" - while os.path.exists(new_path): - if new_name == publisher_id: - new_folder_name += f"_duplicated" - else: - new_folder_name += f"_{publisher_id}" - if folder_name == new_folder_name: - rename_flag = False - break - new_path = os.path.join(folder_path, new_folder_name) + if j > 0: + newname += '_' + str(j) - if rename_flag: - os.rename(folder_to_rename, new_path) - processed_names.add(new_folder_name) + prep_rename(changelist, currFolder[i], newname) + else: + prep_rename( + changelist, currFolder[i], Names[i]+f"_{IDs[i]}") + + for n in changelist: + safe_name = nextnonexistentdir(*tuple(reversed(os.path.split(n[1])))) + if safe_name[0] in ignored_folders and safe_name[1] > 0: + os.rename(n[0], os.path.join(n[0], '{}_{}'.format(*safe_name))) + else: + os.rename(n[0], n[1]) return 1 def change_folder_naming(self, option): main_app.app.title("T7x Workshop Downloader - Settings ➜ Loading... ⏳") try: - if os.path.exists(main_app.app.edit_destination_folder.get()): - lib = main_app.app.library_tab.load_items(main_app.app.edit_destination_folder.get(), dont_add=True) + if os.path.exists(self.edit_destination_folder.get()): + lib = main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) if not "No items" in lib: if show_message("Renaming", "Would you like to rename all your exisiting item folders now?", _return=True): main_app.app.title("T7x Workshop Downloader - Settings ➜ Renaming... ⏳") @@ -476,13 +600,13 @@ class SettingsTab(ctk.CTkFrame): return 0 else: show_message("Done!", "All folders have been renamed", icon="info") - main_app.app.library_tab.load_items(main_app.app.edit_destination_folder.get(), dont_add=True) + main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) else: show_message("Heads up!", "Only newly downloaded items will be affected", icon="info") else: - show_message("Warning -> Check T7x path", f"You don't have any items yet ,from now on item's folders will be named as their {option}") + show_message("Warning -> Check game path", f"You don't have any items yet ,from now on item's folders will be named as their {option}") else: - show_message("Warning -> Check T7x path", f"You don't have any items yet ,from now on item's folders will be named as their {option}") + show_message("Warning -> Check game path", f"You don't have any items yet ,from now on item's folders will be named as their {option}") except Exception as e: show_message("Error", f"Error occured \n{e}") finally: @@ -490,7 +614,7 @@ class SettingsTab(ctk.CTkFrame): self.save_settings() def load_on_switch_screen(self): - self.check_updates_var.set(self.load_settings("checkforupdtes")) + self.check_updates_var.set(self.load_settings("checkforupdates")) self.console_var.set(self.load_settings("console")) self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "10")) self.estimated_progress_var.set(self.load_settings("estimated_progress", "on")) @@ -502,20 +626,20 @@ class SettingsTab(ctk.CTkFrame): # keep last cuz of trace_add() self.save_button.configure(state='disabled') - def settings_launch_T7x(self): - launch_T7x_func(check_config("destinationfolder")) + def settings_launch_game(self): + launch_game_func(check_config("destinationfolder"), self.edit_startup_exe.get(), self.edit_launch_args.get()) def settings_reset_steamcmd(self): reset_steamcmd() - def from_steam_to_T7x_toplevel(self): + def workshop_to_gamedir_toplevel(self): try: # to make sure json file is up to date - main_app.app.library_tab.load_items(main_app.app.edit_destination_folder.get(), dont_add=True) + main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) top = ctk.CTkToplevel(self) if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")): top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico"))) - top.title("Steam to T7x") + top.title("Workshop Transfer") _, _, x, y = get_window_size_from_registry() top.geometry(f"+{x}+{y}") # top.attributes('-topmost', 'true') @@ -527,9 +651,10 @@ class SettingsTab(ctk.CTkFrame): steam_folder_label = ctk.CTkLabel(center_frame, text="Steam Folder:") steam_folder_entry = ctk.CTkEntry(center_frame, width=225) button_steam_browse = ctk.CTkButton(center_frame, text="Select", width=10) - T7x_folder_label = ctk.CTkLabel(center_frame, text="T7x Folder:") - T7x_folder_entry = ctk.CTkEntry(center_frame, width=225) + game_folder_label = ctk.CTkLabel(center_frame, text="Game Folder:") + game_folder_entry = ctk.CTkEntry(center_frame, width=225) button_T7x_browse = ctk.CTkButton(center_frame, text="Select", width=10) + # Create option to choose between cut or copy operation_label = ctk.CTkLabel(center_frame, text="Choose operation:") copy_var = ctk.BooleanVar() @@ -574,11 +699,11 @@ class SettingsTab(ctk.CTkFrame): if copy_var.get(): copy_button.configure(text=f"Start (Copy)") - def open_T7x_browser(): - selected_folder = ctk.filedialog.askdirectory(title="Select T7x Folder") + def open_game_browser(): + selected_folder = ctk.filedialog.askdirectory(title="Select Game Folder") if selected_folder: - T7x_folder_entry.delete(0, "end") - T7x_folder_entry.insert(0, selected_folder) + game_folder_entry.delete(0, "end") + game_folder_entry.insert(0, selected_folder) def open_steam_browser(): selected_folder = ctk.filedialog.askdirectory(title="Select Steam Folder (ex: C:/Program Files (x86)/Steam)") @@ -591,20 +716,20 @@ class SettingsTab(ctk.CTkFrame): def start_thread(): try: if not cut_var.get() and not copy_var.get(): - show_message("Choose operation!", "Please choose an operation, Copy or Cut files from steam!") + show_message("Choose operation!", "Please choose an operation, Copy or Cut files from Steam!") return copy_button.configure(state="disabled") steam_folder = steam_folder_entry.get() ws_folder = os.path.join(steam_folder, "steamapps/workshop/content/311210") - T7x_folder = T7x_folder_entry.get() + game_folder = game_folder_entry.get() if not os.path.exists(steam_folder) and not os.path.exists(ws_folder): show_message("Not found", "Either you have no items downloaded from Steam or wrong path, please recheck path (ex: C:/Program Files (x86)/Steam)") return - if not os.path.exists(T7x_folder): - show_message("Not found", "T7x folder not found, please recheck path") + if not os.path.exists(game_folder): + show_message("Not found", "game folder not found, please recheck path") return top.after(0, progress_text.configure(text="Loading...")) @@ -644,10 +769,10 @@ class SettingsTab(ctk.CTkFrame): folder_name = extract_json_data(json_file_path, "publisherID") if mod_type == "mod": - path_folder = os.path.join(T7x_folder, "mods") + path_folder = os.path.join(game_folder, "mods") folder_name_path = os.path.join(path_folder, folder_name, "zone") elif mod_type == "map": - path_folder = os.path.join(T7x_folder, "usermaps") + path_folder = os.path.join(game_folder, "usermaps") folder_name_path = os.path.join(path_folder, folder_name, "zone") else: show_message("Error", "Invalid workshop type in workshop.json, are you sure this is a map or a mod?.", icon="cancel") @@ -669,17 +794,17 @@ class SettingsTab(ctk.CTkFrame): if cut_var.get(): remove_tree(os.path.join(map_folder, dir_name)) - main_app.app.library_tab.update_item(main_app.app.edit_destination_folder.get(), workshop_id, mod_type, folder_name) + main_app.app.library_tab.update_item(self.edit_destination_folder.get(), workshop_id, mod_type, folder_name) else: # if its last folder to check if i == total_folders: show_message("Error", f"workshop.json not found in {dir_name}", icon="cancel") - main_app.app.library_tab.load_items(main_app.app.edit_destination_folder.get(), dont_add=True) + main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) return continue if subfolders: - main_app.app.library_tab.load_items(main_app.app.edit_destination_folder.get(), dont_add=True) + main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) main_app.app.show_complete_message(message=f"All items were moved\nYou can run the game now!\nPS: You have to restart the game\n(pressing launch will launch/restarts)") finally: @@ -699,8 +824,8 @@ class SettingsTab(ctk.CTkFrame): button_steam_browse.grid(row=1, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes") steam_folder_label.grid(row=0, column=0, padx=(20, 20), pady=(10, 0), sticky='w') steam_folder_entry.grid(row=1, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes') - T7x_folder_label.grid(row=2, column=0, padx=(20, 20), pady=(10, 0), sticky='w') - T7x_folder_entry.grid(row=3, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes') + game_folder_label.grid(row=2, column=0, padx=(20, 20), pady=(10, 0), sticky='w') + game_folder_entry.grid(row=3, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes') button_T7x_browse.grid(row=3, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes") operation_label.grid(row=4, column=0, padx=(20, 20), pady=(10, 10), sticky='wnes') copy_check.grid(row=4, column=1, padx=(0, 10), pady=(10, 10), sticky='wnes') @@ -711,14 +836,14 @@ class SettingsTab(ctk.CTkFrame): progress_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="T7xwd_theme.json")), "progress_bar_fill_color") progress_bar.configure(progress_color=progress_color) steam_folder_entry.insert(1, check_config("steam_folder", "")) - T7x_folder_entry.insert(1, main_app.app.edit_destination_folder.get()) - button_T7x_browse.configure(command=open_T7x_browser) + game_folder_entry.insert(1, self.edit_destination_folder.get()) + button_T7x_browse.configure(command=open_game_browser) button_steam_browse.configure(command=open_steam_browser) copy_button.configure(command=start_copy_operation) cut_check.configure(command = lambda: check_status(cut_var, copy_var)) copy_check.configure(command = lambda: check_status(copy_var, cut_var)) main_app.app.create_context_menu(steam_folder_entry) - main_app.app.create_context_menu(T7x_folder_entry) + main_app.app.create_context_menu(game_folder_entry) copy_var.set(True) progress_bar.set(0) top.after(150, top.focus_force)