diff --git a/.gitignore b/.gitignore
index 7fc6638..6db02d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -161,4 +161,6 @@ cython_debug/
# Other
*.ini
-dist/
\ No newline at end of file
+*.conf
+dist/
+.vscode
\ No newline at end of file
diff --git a/CTkToolTip/__pycache__/__init__.cpython-311.pyc b/CTkToolTip/__pycache__/__init__.cpython-311.pyc
deleted file mode 100644
index 2620486..0000000
Binary files a/CTkToolTip/__pycache__/__init__.cpython-311.pyc and /dev/null differ
diff --git a/CTkToolTip/__pycache__/ctk_tooltip.cpython-311.pyc b/CTkToolTip/__pycache__/ctk_tooltip.cpython-311.pyc
deleted file mode 100644
index 756efbe..0000000
Binary files a/CTkToolTip/__pycache__/ctk_tooltip.cpython-311.pyc and /dev/null differ
diff --git a/README.md b/README.md
index 9125149..3bcb3cf 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
## Usage (script):
- ```pip install -r requirements.txt``` -> use my modified [CTkToolTip](./CTkToolTip) and [CTkListbox](./CTkListbox) for [v0.2.8](https://github.com/faroukbmiled/BOIIIWD/releases) and up
-- ```python boiiiwd.py```
+- ```python boiiiwd_package\boiiiwd.py```
- Slap in your workshop item link for example: "https://steamcommunity.com/sharedfiles/filedetails/?id=3011930738" or just the id 3011930738)
## Features:
@@ -36,16 +36,16 @@
## Freezing into an exe (pyinstaller):
-- ```pip install -r requirements.txt``` -> use my modified [CTkToolTip](./CTkToolTip) and [CTkListbox](./CTkListbox) for [v0.2.8](https://github.com/faroukbmiled/BOIIIWD/releases) and up
-- ```pip install pyinstaller```
-- ```pyinstaller --noconfirm --onefile --windowed --icon "ryuk.ico" --name "BOIIIWD" --ascii "boiiiwd.py" --add-data "resources;resources" --add-data "c:\\lib\site-packages\customtkinter;customtkinter\" --add-data "c:\\lib\site-packages\CTkMessagebox;CTkMessagebox\" --add-data "c:\\lib\site-packages\CTkToolTip;CTkToolTip\"```
+- ```pip install -r requirements.txt``` -> use my modified [CTkToolTip](./CTkToolTip) and [CTkListbox](./CTkListbox) for [v0.2.8](https://github.com/faroukbmiled/BOIIIWD/releases) and up.
+- ```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,2113146805``` or
+```3010399939,2976006537,2118338989```
+or
```3010399939
2976006537
2118338989
diff --git a/boiiiwd.py b/boiiiwd.py
deleted file mode 100644
index 76a9e64..0000000
--- a/boiiiwd.py
+++ /dev/null
@@ -1,3485 +0,0 @@
-# Use CTkToolTip and CTkListbox from my repo originally by Akascape (https://github.com/Akascape)
-from CTkMessagebox import CTkMessagebox
-from tkinter import Menu, END, Event
-from bs4 import BeautifulSoup
-from datetime import datetime
-import customtkinter as ctk
-from pathlib import Path
-from CTkToolTip import *
-from CTkListbox import *
-from PIL import Image
-import configparser
-import webbrowser
-import subprocess
-import threading
-import requests
-import zipfile
-import shutil
-import psutil
-import json
-import math
-import time
-import sys
-import io
-import os
-import re
-
-VERSION = "v0.3.1"
-GITHUB_REPO = "faroukbmiled/BOIIIWD"
-LATEST_RELEASE_URL = "https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip"
-UPDATER_FOLDER = "update"
-CONFIG_FILE_PATH = "config.ini"
-RESOURCES_DIR = os.path.join(os.path.dirname(__file__), 'resources')
-LIBRARY_FILE = "boiiiwd_library.json"
-
-# Start Helper Functions
-def cwd():
- if getattr(sys, 'frozen', False):
- return os.path.dirname(sys.executable)
- else:
- return os.path.dirname(os.path.abspath(__file__))
-
-def check_config(name, fallback=None):
- config = configparser.ConfigParser()
- config.read(CONFIG_FILE_PATH)
- if fallback:
- 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)
- if name and value:
- config.set("Settings", 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(cwd(), theme_name)):
- return os.path.join(cwd(), theme_name)
- else:
- try:
- return os.path.join(RESOURCES_DIR, theme_name)
- except:
- return os.path.join(RESOURCES_DIR, "boiiiwd_theme.json")
-
-# theme initialization
-ctk.set_appearance_mode(check_config("appearance", "Dark")) # Modes: "System" (standard), "Dark", "Light"
-try:
- ctk.set_default_color_theme(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")))
-except:
- save_config("theme", "boiiiwd_theme.json")
- ctk.set_default_color_theme(os.path.join(RESOURCES_DIR, "boiiiwd_theme.json"))
-
-def get_latest_release_version():
- try:
- 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()
- return data["tag_name"]
- except requests.exceptions.RequestException as e:
- 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
- echo Terminating BOIIIWD.exe...
- taskkill /im "{program_name}" /t /f
-
- echo Replacing BOIIIWD.exe...
- cd "{updater_folder}"
- taskkill /im "{program_name}" /t /f
- move /y "{new_exe}" "../"{program_name}""
-
- echo Starting BOIIIWD.exe...
- cd ..
- start "" "{current_exe}"
-
- echo Exiting!
- exit
- """
-
- script_path = os.path.join(updater_folder, "boiiiwd_updater.bat")
- with open(script_path, "w") as script_file:
- script_file.write(script_content)
-
- return script_path
-
-def is_internet_available():
- try:
- requests.get("https://www.google.com", timeout=3)
- return True
- except:
- return False
-
-def check_for_updates_func(window, ignore_up_todate=False):
- if not is_internet_available():
- show_message("Error!", "Internet connection is not available. Please check your internet connection and try again.")
- return
- 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(".", ""))
-
- 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)
-
- result = msg_box.get()
-
- if result == "View":
- webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest")
-
- if result == "Yes":
- update_window = UpdateWindow(window, LATEST_RELEASE_URL)
- update_window.start_update()
-
- if result == "No":
- return
-
- elif int_latest_version < int_current_version:
- if ignore_up_todate:
- return
- msg_box = CTkMessagebox(title="Up to Date!", message=f"Unreleased version!\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="Ok", sound=True)
- result = msg_box.get()
- 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)
- result = msg_box.get()
-
- else:
- 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+)'
- match = re.search(pattern, link)
-
- if match:
- return match.group(0)
- else:
- return None
- except:
- return None
-
-def check_steamcmd():
- steamcmd_path = get_steamcmd_path()
- steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe")
-
- if not os.path.exists(steamcmd_exe_path):
- return False
-
- 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)
- master.attributes('-alpha', 0.0)
- process.wait()
- if is_steamcmd_initialized():
- show_message("SteamCMD has terminated!", "BOIIIWD is ready for action.", icon="info")
- else:
- 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")
- master.attributes('-alpha', 1.0)
-
-
-def valid_id(workshop_id):
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
- response = requests.get(url)
- response.raise_for_status()
- content = response.text
- soup = BeautifulSoup(content, "html.parser")
-
- try:
- soup.find("div", class_="rightDetailsBlock").text.strip()
- soup.find("div", class_="workshopItemTitle").text.strip()
- soup.find("div", class_="detailsStatRight").text.strip()
- stars_div = soup.find("div", class_="fileRatingDetails")
- stars_div.find("img")["src"]
- return True
- except:
- return False
-
-def convert_speed(speed_bytes):
- if speed_bytes < 1024:
- return speed_bytes, "B/s"
- elif speed_bytes < 1024 * 1024:
- return speed_bytes / 1024, "KB/s"
- elif speed_bytes < 1024 * 1024 * 1024:
- return speed_bytes / (1024 * 1024), "MB/s"
- else:
- return speed_bytes / (1024 * 1024 * 1024), "GB/s"
-
-def create_default_config():
- config = configparser.ConfigParser()
- config["Settings"] = {
- "SteamCMDPath": cwd(),
- "DestinationFolder": "",
- "checkforupdtes": "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=cwd())
-
-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:
- if no_symb:
- return f"{size_in_bytes:.2f}"
- 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)
- soup = BeautifulSoup(response.text, "html.parser")
- file_size_element = soup.find("div", class_="detailsStatRight")
-
- try:
- 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)
- 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)
- return file_size_in_bytes
- return None
- except:
- 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)
- response = msg.get()
- if response == option_1:
- return False
- elif response == option_2:
- return True
- else:
- return False
- else:
- def callback():
- CTkMessagebox(title=title, message=message, icon=icon, sound=True)
- app.after(0, callback)
-
-def launch_boiii_func(path):
- procname = "boiii.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()
- boiii_path = os.path.join(path, procname)
- subprocess.Popen([boiii_path ,"-launch"] , 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 BOIII", f"Failed to launch boiii.exe\nMake sure to put in your correct boiii path\n{e}")
-
-def remove_tree(folder_path, show_error=None):
- if show_error:
- try:
- shutil.rmtree(folder_path)
- except Exception as e:
- show_message("Error!", f"An error occurred while trying to remove files:\n{e}", icon="cancel")
- try:
- shutil.rmtree(folder_path)
- 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):
- for f in files:
- fp = os.path.join(path, f)
- 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")
- steamcmd_size = os.path.getsize(steamcmd_exe_path)
- if steamcmd_size < 3 * 1024 * 1024:
- return False
- return True
-
-def get_button_state_colors(file_path, state):
- try:
- with open(file_path, 'r') as json_file:
- data = json.load(json_file)
- if 'BOIIIWD_Globals' in data:
- boiiiwd_globals = data['BOIIIWD_Globals']
- if state in boiiiwd_globals:
- return boiiiwd_globals[state]
- else:
- return None
- else:
- return None
- except FileNotFoundError:
- return None
- 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",]
-
- for directory in directories_to_reset:
- directory_path = os.path.join(steamcmd_path, directory)
- if os.path.exists(directory_path):
- remove_tree(directory_path, show_error=True)
-
- for root, _, files in os.walk(steamcmd_path):
- for filename in files:
- if filename.endswith((".old", ".crash")):
- file_path = os.path.join(root, filename)
- os.remove(file_path)
-
- if not no_warn:
- 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")
-
-def get_item_name(id):
- try:
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={id}"
- response = requests.get(url)
- response.raise_for_status()
- content = response.text
-
- soup = BeautifulSoup(content, "html.parser")
-
- try:
- map_name = soup.find("div", class_="workshopItemTitle").text.strip()
- name = map_name[:32] + "..." if len(map_name) > 32 else map_name
- return name
- except:
- return True
- except:
- 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 check_item_date(down_date, date_updated):
- 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"
- try:
- try:
- 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)
-
- try:
- upload_datetime = datetime.strptime(date_updated, date_format_with_year)
- except ValueError:
- upload_datetime = datetime.strptime(date_updated + f", {current_year}", date_format_with_added_year)
-
- if upload_datetime >= download_datetime:
- return True
- elif upload_datetime < download_datetime:
- return False
- except:
- return False
-
-# End helper functions
-class UpdateWindow(ctk.CTkToplevel):
- def __init__(self, master, update_url):
- global master_win
- master_win = master
- super().__init__(master)
- self.title("BOIIIWD Self-Updater")
- self.geometry("400x150")
- if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")):
- self.after(250, lambda: self.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")))
- self.protocol("WM_DELETE_WINDOW", self.cancel_update)
- self.attributes('-topmost', 'true')
-
- self.columnconfigure(0, weight=1)
- self.columnconfigure(1, weight=1)
- self.rowconfigure(0, weight=1)
- self.rowconfigure(1, weight=1)
-
- self.label_download = ctk.CTkLabel(self, text="Starting...")
- self.label_download.grid(row=0, column=0, padx=30, pady=(10, 0), sticky="w")
-
- self.label_size = ctk.CTkLabel(self, text="Size: 0")
- self.label_size.grid(row=0, column=1, padx=30, pady=(10, 0), sticky="e")
-
- self.progress_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "progress_bar_fill_color")
- self.progress_bar = ctk.CTkProgressBar(self, mode="determinate", height=20, corner_radius=7, progress_color=self.progress_color)
- self.progress_bar.grid(row=1, column=0, columnspan=4, padx=30, pady=10, sticky="ew")
- self.progress_bar.set(0)
-
- self.progress_label = ctk.CTkLabel(self.progress_bar, text="0%", font=("Helvetica", 12), fg_color="transparent", height=0, width=0, corner_radius=0)
- self.progress_label.place(relx=0.5, rely=0.5, anchor="center")
-
- self.cancel_button = ctk.CTkButton(self, text="Cancel", command=self.cancel_update)
- self.cancel_button.grid(row=2, column=0, padx=30, pady=(0, 10), sticky="w")
-
- self.update_url = update_url
- self.total_size = None
- self.up_cancelled = False
-
- def update_progress_bar(self):
- try:
- update_dir = os.path.join(os.getcwd(), UPDATER_FOLDER)
- response = requests.get(LATEST_RELEASE_URL, stream=True)
- response.raise_for_status()
- current_exe = sys.argv[0]
- program_name = os.path.basename(current_exe)
- new_exe = os.path.join(update_dir, "BOIIIWD.exe")
-
- if not os.path.exists(update_dir):
- os.makedirs(update_dir)
-
- self.progress_bar.set(0.0)
- self.total_size = int(response.headers.get('content-length', 0))
- self.label_size.configure(text=f"Size: {convert_bytes_to_readable(self.total_size)}")
- zip_path = os.path.join(update_dir, "latest_version.zip")
-
- with open(zip_path, "wb") as file:
- downloaded_size = 0
- for chunk in response.iter_content(chunk_size=8192):
- if self.up_cancelled:
- break
- if chunk:
- file.write(chunk)
- downloaded_size += len(chunk)
- progress = int((downloaded_size / self.total_size) * 100)
-
- self.after(1, lambda p=progress: self.label_download.configure(text=f"Downloading update..."))
- self.after(1, lambda v=progress / 100.0: self.progress_bar.set(v))
- self.after(1, lambda p=progress: self.progress_label.configure(text=f"{p}%"))
-
- if not self.up_cancelled:
- self.progress_bar.set(1.0)
- with zipfile.ZipFile(zip_path, "r") as zip_ref:
- zip_ref.extractall(update_dir)
- self.label_download.configure(text="Update Downloaded successfully!")
- def update_msg():
- msg = CTkMessagebox(title="Success!", message="Update Downloaded successfully!\nPress ok to install it", icon="info", option_1="No", option_2="Ok", sound=True)
- response = msg.get()
- if response == "No":
- self.destroy()
- return
- elif response == "Ok":
- script_path = create_update_script(current_exe, new_exe, update_dir, program_name)
- subprocess.run(('cmd', '/C', 'start', '', fr'{script_path}'))
- sys.exit(0)
- else:
- return
- self.after(0, update_msg)
- return
- else:
- if os.path.exists(zip_path):
- os.remove(fr"{zip_path}")
- self.label_download.configure(text="Update cancelled.")
- self.progress_bar.set(0.0)
- # there's a better solution ill implement it later
- global master_win
- try:
- master_win.attributes('-alpha', 1.0)
- except:
- pass
- show_message("Cancelled!", "Update cancelled by user", icon="warning")
- except Exception as e:
- self.progress_bar.set(0.0)
- self.label_download.configure(text="Update failed")
- show_message("Error", f"Error installing the update\n{e}", icon="cancel")
-
- def start_update(self):
- self.thread = threading.Thread(target=self.update_progress_bar)
- self.thread.start()
-
- def cancel_update(self):
- self.up_cancelled = True
- self.withdraw()
-
-class LibraryTab(ctk.CTkScrollableFrame):
- def __init__(self, master, **kwargs):
-
- super().__init__(master, **kwargs)
- self.added_items = set()
- self.to_update = set()
- self.grid_columnconfigure(0, weight=1)
-
- self.radiobutton_variable = ctk.StringVar()
- self.no_items_label = ctk.CTkLabel(self, text="", anchor="w")
- self.filter_entry = ctk.CTkEntry(self, placeholder_text="Your search query here, or type in mod or map to only see that")
- self.filter_entry.bind("", self.filter_items)
- self.filter_entry.grid(row=0, column=0, padx=(10, 20), pady=(10, 20), sticky="we")
- filter_refresh_button_image = os.path.join(RESOURCES_DIR, "Refresh_icon.svg.png")
- update_button_image = os.path.join(RESOURCES_DIR, "update_icon.png")
- self.filter_refresh_button = ctk.CTkButton(self, image=ctk.CTkImage(Image.open(filter_refresh_button_image)), command=self.refresh_items, width=20, height=20,
- fg_color="transparent", text="")
- self.filter_refresh_button.grid(row=0, column=1, padx=(10, 0), pady=(10, 20), sticky="nw")
- self.update_button = ctk.CTkButton(self, image=ctk.CTkImage(Image.open(update_button_image)), command=self.check_for_updates, width=65, height=20,
- text="", fg_color="transparent")
- self.update_button.grid(row=0, column=1, padx=(0, 20), pady=(10, 20), sticky="en")
- self.update_tooltip = CTkToolTip(self.update_button, message="Check items for updates", topmost=True)
- filter_tooltip = CTkToolTip(self.filter_refresh_button, message="Refresh library", topmost=True)
- self.label_list = []
- self.button_list = []
- self.button_view_list = []
- self.file_cleaned = False
- self.filter_type = True
- self.clipboard_has_content = False
- self.item_block_list = set()
- self.added_folders = set()
- self.ids_added = set()
-
- def add_item(self, item, image=None, workshop_id=None, folder=None, invalid_warn=False):
- label = ctk.CTkLabel(self, text=item, image=image, compound="left", padx=5, anchor="w")
- button = ctk.CTkButton(self, text="Remove", width=60, height=24, fg_color="#3d3f42")
- button_view = ctk.CTkButton(self, text="Details", width=55, height=24, fg_color="#3d3f42")
- button.configure(command=lambda: self.remove_item(item, folder, workshop_id))
- button_view.configure(command=lambda: self.show_map_info(workshop_id, invalid_warn))
- button_view_tooltip = CTkToolTip(button_view, message="Opens up a window that shows basic details")
- button_tooltip = CTkToolTip(button, message="Removes the map/mod from your game")
- label.grid(row=len(self.label_list) + 1, column=0, pady=(0, 10), padx=(5, 10), sticky="w")
- button.grid(row=len(self.button_list) + 1, column=1, pady=(0, 10), padx=(50, 10), sticky="e")
- button_view.grid(row=len(self.button_view_list) + 1, column=1, pady=(0, 10), padx=(10, 75), sticky="w")
- self.label_list.append(label)
- self.button_list.append(button)
- self.button_view_list.append(button_view)
- label.bind("", lambda event, label=label: self.on_label_hover(label, enter=True))
- label.bind("", lambda event, label=label: self.on_label_hover(label, enter=False))
- label.bind("", lambda event, label=label: self.copy_to_clipboard(label, workshop_id, event))
- label.bind("", lambda event, label=label: self.copy_to_clipboard(label, workshop_id, event, append=True))
- label.bind("", lambda event: self.open_folder_location(folder, event))
- label.bind("", lambda event, label=label: self.copy_to_clipboard(label, folder, event))
- if invalid_warn:
- label_warn = CTkToolTip(label, message="Duplicated or Blocked item (Search item id in search)")
-
- def on_label_hover(self, label, enter):
- if enter:
- label.configure(fg_color="#272727")
- else:
- label.configure(fg_color="transparent")
-
- def copy_to_clipboard(self, label, something, event=None, append=False):
- try:
- if append:
- if self.clipboard_has_content:
- label.clipboard_append(f"\n{something}")
- show_noti(label, "Appended to clipboard", event, 1.0)
- else:
- label.clipboard_clear()
- label.clipboard_append(something)
- self.clipboard_has_content = True
- show_noti(label, "Copied to clipboard", event, 1.0)
- else:
- label.clipboard_clear()
- label.clipboard_append(something)
- self.clipboard_has_content = True
- show_noti(label, "Copied to clipboard", event, 1.0)
- except:
- pass
-
- def open_folder_location(self, folder, event=None):
- if os.path.exists(folder):
- os.startfile(folder)
- show_noti(self, "Opening folder", event, 1.0)
-
- def item_exists_in_file(self, items_file, workshop_id, folder_name=None):
- if not os.path.exists(items_file):
- return False, False
-
- with open(items_file, "r") as f:
- items_data = json.load(f)
- for item_info in items_data:
- if "id" in item_info and "folder_name" in item_info and "json_folder_name" in item_info:
- if item_info["id"] == workshop_id and item_info["folder_name"] == folder_name:
- if item_info["folder_name"] in self.added_folders:
- continue
- if item_info["folder_name"] in self.item_block_list:
- return False ,None
- return True, True
- elif item_info["id"] == workshop_id:
- if item_info["folder_name"] in self.added_folders:
- continue
- if item_info["folder_name"] in self.item_block_list:
- return False ,None
- return True, False
-
- elif "id" in item_info and item_info["id"] == workshop_id:
- return True, False
- return False, False
-
- def remove_item_by_option(self, items_file, option, option_name="id"):
-
- if not os.path.exists(items_file):
- return
-
- with open(items_file, "r") as f:
- items_data = json.load(f)
-
- updated_items_data = [item for item in items_data if item.get(option_name) != option]
-
- if len(updated_items_data) < len(items_data):
- with open(items_file, "w") as f:
- json.dump(updated_items_data, f, indent=4)
-
- def get_item_by_id(self, items_file, item_id, return_option="all"):
-
- if not os.path.exists(items_file):
- return None
-
- with open(items_file, "r") as f:
- items_data = json.load(f)
-
- for item in items_data:
- if item.get("id") == item_id:
- if return_option == "all":
- return item
- elif return_option == return_option:
- return item.get(return_option)
- return None
-
- def get_item_index_by_id(self, items_data, item_id):
- for index, item in enumerate(items_data):
- if item.get("id") == item_id:
- return index
- return None
-
- def update_or_add_item_by_id(self, items_file, item_info, item_id):
- if not os.path.exists(items_file):
- with open(items_file, "w") as f:
- json.dump([item_info], f, indent=4)
- else:
- with open(items_file, "r+") as f:
- items_data = json.load(f)
- existing_item_index = self.get_item_index_by_id(items_data, item_id)
- if existing_item_index is not None:
- items_data[existing_item_index] = item_info
- else:
- items_data.append(item_info)
- f.seek(0)
- f.truncate()
- json.dump(items_data, f, indent=4)
-
- def clean_json_file(self, file):
-
- if not os.path.exists(file):
- show_message("Error", f"File '{file}' does not exist.")
- return
-
- with open(file, "r") as f:
- items_data = json.load(f)
-
- cleaned_items = [item for item in items_data if 'folder_name' in item and 'json_folder_name'
- in item and item['folder_name'] not in self.item_block_list and item['folder_name'] in self.added_folders]
-
- with open(file, 'w') as file:
- json.dump(cleaned_items, file, indent=4)
-
- def filter_items(self, event):
- filter_text = self.filter_entry.get().lower()
- for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list):
- item_text = label.cget("text").lower()
- if filter_text in item_text:
- label.grid()
- button.grid()
- button_view_list.grid()
- else:
- label.grid_remove()
- button_view_list.grid_remove()
- button.grid_remove()
-
- def load_items(self, boiiiFolder):
- maps_folder = Path(boiiiFolder) / "mods"
- mods_folder = Path(boiiiFolder) / "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")
- b_map_img = os.path.join(RESOURCES_DIR, "b_map_image.png")
- map_count = 0
- mod_count = 0
- total_size = 0
-
- folders_to_process = [mods_folder, maps_folder]
-
- items_file = os.path.join(cwd(), LIBRARY_FILE)
-
- for folder_path in folders_to_process:
- for zone_path in folder_path.glob("**/zone"):
- json_path = zone_path / "workshop.json"
- if json_path.exists():
- # current folder name
- curr_folder_name = zone_path.parent.name
-
- workshop_id = extract_json_data(json_path, "PublisherID")
- name = extract_json_data(json_path, "Title").replace(">", "").replace("^", "")
- name = name[:45] + "..." if len(name) > 45 else name
- item_type = extract_json_data(json_path, "Type")
- folder_name = extract_json_data(json_path, "FolderName")
- folder_size_bytes = get_folder_size(zone_path.parent)
- size = convert_bytes_to_readable(folder_size_bytes)
- total_size += folder_size_bytes
- text_to_add = f"{name} | Type: {item_type.capitalize()}"
- mode_type = "ZM" if item_type == "map" and folder_name.startswith("zm") else "MP" if folder_name.startswith("mp") and item_type == "map" else None
- if mode_type:
- text_to_add += f" | Mode: {mode_type}"
- text_to_add += f" | ID: {workshop_id} | Size: {size}"
-
- creation_timestamp = None
- for ff_file in zone_path.glob("*.ff"):
- if ff_file.exists():
- creation_timestamp = ff_file.stat().st_ctime
- break
-
- if creation_timestamp is not None:
- date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
- else:
- creation_timestamp = zone_path.stat().st_ctime
- date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
-
- map_count += 1 if item_type == "map" else 0
- mod_count += 1 if item_type == "mod" else 0
- 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()):
- try: self.remove_item_by_option(items_file, curr_folder_name, "folder_name")
- except: pass
- self.item_block_list.add(curr_folder_name)
- image_path = b_mod_img if item_type == "mod" else b_map_img
- text_to_add += " | ⚠️"
- elif curr_folder_name not in self.added_folders and workshop_id in self.ids_added:
- try: self.remove_item_by_option(items_file, curr_folder_name, "folder_name")
- except: pass
- image_path = b_mod_img if item_type == "mod" else b_map_img
- text_to_add += " | ⚠️"
-
- self.added_items.add(text_to_add)
- if image_path is b_mod_img or image_path is b_map_img:
- self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), workshop_id=workshop_id, folder=zone_path.parent, invalid_warn=True)
- else:
- self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), workshop_id=workshop_id, folder=zone_path.parent)
- id_found, folder_found = self.item_exists_in_file(items_file, workshop_id, curr_folder_name)
- item_info = {
- "id": workshop_id,
- "text": text_to_add,
- "date": date_added,
- "folder_name": curr_folder_name,
- "json_folder_name": folder_name
- }
- # 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:
- if not os.path.exists(items_file):
- with open(items_file, "w") as f:
- json.dump([item_info], f, indent=4)
- else:
- with open(items_file, "r+") as f:
- items_data = json.load(f)
- items_data.append(item_info)
- 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:
- 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)
- if not workshop_id in self.ids_added:
- self.ids_added.add(workshop_id)
-
- if not self.file_cleaned and os.path.exists(items_file):
- self.file_cleaned = True
- self.clean_json_file(items_file)
-
- if not self.added_items:
- self.show_no_items_message()
- else:
- self.hide_no_items_message()
-
- if map_count > 0 or mod_count > 0:
- return f"Maps: {map_count} - Mods: {mod_count} - Total size: {convert_bytes_to_readable(total_size)}"
- return "No items in current selected folder"
-
- def update_item(self, boiiiFolder, id, item_type, foldername):
- try:
- if item_type == "map":
- folder_path = Path(boiiiFolder) / "usermaps" / f"{foldername}"
- elif item_type == "mod":
- folder_path = Path(boiiiFolder) / "mods" / f"{foldername}"
- else:
- raise ValueError("Unsupported item_type. It must be 'map' or 'mod'.")
-
- 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
- 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))
- text_to_add = f"{name} | Type: {item_type.capitalize()}"
- mode_type = "ZM" if item_type == "map" and folder_name.startswith("zm") else "MP" if folder_name.startswith("mp") and item_type == "map" else None
- if mode_type:
- text_to_add += f" | Mode: {mode_type}"
- text_to_add += f" | ID: {workshop_id} | Size: {size}"
- date_added = datetime.now().strftime("%d %b, %Y @ %I:%M%p")
- items_file = os.path.join(cwd(), LIBRARY_FILE)
-
- item_info = {
- "id": workshop_id,
- "text": text_to_add,
- "date": date_added,
- "folder_name": foldername,
- "json_folder_name": folder_name
- }
- self.update_or_add_item_by_id(items_file, item_info, id)
- return
-
- except Exception as e:
- show_message("Error updating json file", f"Error while updating library json file\n{e}")
-
- def remove_item(self, item, folder, id):
- items_file = os.path.join(cwd(), LIBRARY_FILE)
- for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list):
- if item == label.cget("text"):
- self.added_folders.remove(os.path.basename(folder))
- try:
- shutil.rmtree(folder)
- except Exception as e:
- show_message("Error" ,f"Error removing folder '{folder}': {e}", icon="cancel")
- return
- label.destroy()
- button.destroy()
- button_view_list.destroy()
- self.label_list.remove(label)
- self.button_list.remove(button)
- self.added_items.remove(label.cget("text"))
- self.ids_added.remove(id)
- self.button_view_list.remove(button_view_list)
- self.remove_item_by_option(items_file, id)
-
- def refresh_items(self):
- for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list):
- label.destroy()
- button.destroy()
- button_view_list.destroy()
- self.label_list.clear()
- self.button_list.clear()
- self.button_view_list.clear()
- self.added_items.clear()
- self.added_folders.clear()
- self.ids_added.clear()
- status = self.load_items(app.edit_destination_folder.get().strip())
- app.title(f"BOIII Workshop Downloader - Library ➜ {status}")
-
- def view_item(self, workshop_id):
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
- webbrowser.open(url)
-
- def show_no_items_message(self):
- 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 boiii folder selected.")
-
- def hide_no_items_message(self):
- self.no_items_label.configure(text="")
- self.no_items_label.forget()
-
- # i know i know ,please make a pull request i cant be bother
- def show_map_info(self, workshop, invalid_warn=False):
- for button_view in self.button_view_list:
- button_view.configure(state="disabled")
-
- def show_map_thread():
- workshop_id = workshop
-
- if not workshop_id.isdigit():
- try:
- if extract_workshop_id(workshop_id).strip().isdigit():
- workshop_id = extract_workshop_id(workshop_id).strip()
- else:
- show_message("Warning", "Not a valid Workshop ID.")
- except:
- show_message("Warning", "Not a valid Workshop ID.")
- return
- try:
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
- response = requests.get(url)
- response.raise_for_status()
- content = response.text
-
- soup = BeautifulSoup(content, "html.parser")
-
- try:
- map_mod_type = soup.find("div", class_="rightDetailsBlock").text.strip()
- map_name = soup.find("div", class_="workshopItemTitle").text.strip()
- map_size = map_size = get_workshop_file_size(workshop_id, raw=True)
- details_stats_container = soup.find("div", class_="detailsStatsContainerRight")
- details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight")
- date_created = details_stat_elements[1].text.strip()
- try:
- ratings = soup.find('div', class_='numRatings')
- ratings_text = ratings.get_text()
- except:
- ratings = "Not found"
- ratings_text= "Not enough ratings"
- try:
- date_updated = details_stat_elements[2].text.strip()
- except:
- date_updated = "Not updated"
- stars_div = soup.find("div", class_="fileRatingDetails")
- starts = stars_div.find("img")["src"]
- except:
- show_message("Warning", "Not a valid Workshop ID\nCouldn't get information.")
- for button_view in self.button_view_list:
- button_view.configure(state="normal")
- return
-
- try:
- preview_image_element = soup.find("img", id="previewImage")
- workshop_item_image_url = preview_image_element["src"]
- except:
- try:
- preview_image_element = soup.find("img", id="previewImageMain")
- workshop_item_image_url = preview_image_element["src"]
- except Exception as e:
- show_message("Warning", f"Failed to get preview image ,probably wrong link/id if not please open an issue on github.\n{e}")
- for button_view in self.button_view_list:
- button_view.configure(state="normal")
- return
-
- starts_image_response = requests.get(starts)
- stars_image = Image.open(io.BytesIO(starts_image_response.content))
- stars_image_size = stars_image.size
-
- image_response = requests.get(workshop_item_image_url)
- image_response.raise_for_status()
- image = Image.open(io.BytesIO(image_response.content))
- image_size = image.size
-
- self.toplevel_info_window(map_name, map_mod_type, map_size, image, image_size, date_created,
- date_updated, stars_image, stars_image_size, ratings_text, url, workshop_id, invalid_warn)
-
- except requests.exceptions.RequestException as e:
- show_message("Error", f"Failed to fetch map information.\nError: {e}", icon="cancel")
- for button_view in self.button_view_list:
- button_view.configure(state="normal")
- return
-
- info_thread = threading.Thread(target=show_map_thread)
- info_thread.start()
-
- def toplevel_info_window(self, map_name, map_mod_type, map_size, image, image_size,
- date_created ,date_updated, stars_image, stars_image_size, ratings_text, url, workshop_id, invalid_warn):
- def main_thread():
- try:
- items_file = os.path.join(cwd(), LIBRARY_FILE)
- 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("Map/Mod Information")
- top.attributes('-topmost', 'true')
- down_date = self.get_item_by_id(items_file, workshop_id, 'date')
-
- def close_window():
- top.destroy()
-
- def view_map_mod():
- webbrowser.open(url)
-
- def check_for_updates():
- try:
-
- if check_item_date(down_date, date_updated):
- if show_message("There is an update.", "Press download to redownload!", icon="info", _return=True, option_1="No", option_2="Download"):
- if app.is_downloading:
- show_message("Error", "Please wait for the current download to finish or stop it then restart.", icon="cancel")
- return
- app.edit_workshop_id.delete(0, "end")
- app.edit_workshop_id.insert(0, workshop_id)
- app.main_button_event()
- app.download_map(update=True)
- top.destroy()
- return
- else:
- show_message("Up to date!", "No updates found!", icon="info")
- except:
- show_message("Up to date!", "No updates found!", icon="info")
-
- # frames
- stars_frame = ctk.CTkFrame(top)
- stars_frame.grid(row=0, column=0, columnspan=2, padx=20, pady=(20, 0), sticky="nsew")
- stars_frame.columnconfigure(0, weight=0)
- stars_frame.rowconfigure(0, weight=1)
-
- image_frame = ctk.CTkFrame(top)
- image_frame.grid(row=1, column=0, columnspan=2, padx=20, pady=0, sticky="nsew")
-
- info_frame = ctk.CTkFrame(top)
- info_frame.grid(row=2, column=0, columnspan=2, padx=20, pady=20, sticky="nsew")
-
- buttons_frame = ctk.CTkFrame(top)
- buttons_frame.grid(row=3, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="nsew")
-
- # fillers
- name_label = ctk.CTkLabel(info_frame, text=f"Name: {map_name}")
- name_label.grid(row=0, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- type_label = ctk.CTkLabel(info_frame, text=f"Type: {map_mod_type}")
- type_label.grid(row=1, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- size_label = ctk.CTkLabel(info_frame, text=f"Size (Workshop): {map_size}")
- size_label.grid(row=2, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- date_created_label = ctk.CTkLabel(info_frame, text=f"Posted: {date_created}")
- date_created_label.grid(row=3, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated}")
- date_updated_label.grid(row=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- date_updated_label = ctk.CTkLabel(info_frame, text=f"Downloaded at: {down_date}")
- date_updated_label.grid(row=5, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- stars_image_label = ctk.CTkLabel(stars_frame)
- stars_width, stars_height = stars_image_size
- stars_image_widget = ctk.CTkImage(stars_image, size=(int(stars_width), int(stars_height)))
- stars_image_label.configure(image=stars_image_widget, text="")
- stars_image_label.pack(side="left", padx=(10, 20), pady=(10, 10))
-
- ratings = ctk.CTkLabel(stars_frame)
- ratings.configure(text=ratings_text)
- ratings.pack(side="right", padx=(10, 20), pady=(10, 10))
-
- image_label = ctk.CTkLabel(image_frame)
- width, height = image_size
- image_widget = ctk.CTkImage(image, size=(int(width), int(height)))
- image_label.configure(image=image_widget, text="")
- image_label.pack(expand=True, fill="both", padx=(10, 20), pady=(10, 10))
-
- # Buttons
- close_button = ctk.CTkButton(buttons_frame, text="View", command=view_map_mod, width=130)
- close_button.grid(row=0, column=0, padx=(20, 20), pady=(10, 10), sticky="n")
-
- update_btn = ctk.CTkButton(buttons_frame, text="Update", command=check_for_updates, width=130)
- update_btn.grid(row=0, column=1, padx=(10, 20), pady=(10, 10), sticky="n")
- update_btn_tooltip = CTkToolTip(update_btn, message="Checks and installs updates of the current selected item (redownload!)", topmost=True)
-
- view_button = ctk.CTkButton(buttons_frame, text="Close", command=close_window, width=130)
- view_button.grid(row=0, column=2, padx=(10, 20), pady=(10, 10), sticky="n")
-
- if invalid_warn:
- update_btn.configure(text="Update", state="disabled")
- update_btn_tooltip.configure(message="Disabled due to item being blocked or duplicated")
-
- top.grid_rowconfigure(0, weight=0)
- top.grid_rowconfigure(1, weight=0)
- top.grid_rowconfigure(2, weight=1)
- top.grid_columnconfigure(0, weight=1)
- top.grid_columnconfigure(1, weight=1)
-
- buttons_frame.grid_rowconfigure(0, weight=1)
- buttons_frame.grid_rowconfigure(1, weight=1)
- buttons_frame.grid_rowconfigure(2, weight=1)
- buttons_frame.grid_columnconfigure(0, weight=1)
- buttons_frame.grid_columnconfigure(1, weight=1)
- buttons_frame.grid_columnconfigure(2, weight=1)
-
- finally:
- for button_view in self.button_view_list:
- button_view.configure(state="normal")
- self.after(0, main_thread)
-
- def check_for_updates(self, on_launch=False):
- if not is_internet_available():
- show_message("Error!", "Internet connection is not available. Please check your internet connection and try again.")
- return
- self.after(1, self.update_button.configure(state="disabled"))
- self.update_tooltip.configure(message='Still loading please wait...')
- cevent = Event()
- cevent.x_root = self.update_button.winfo_rootx()
- cevent.y_root = self.update_button.winfo_rooty()
- if not on_launch:
- show_noti(self.update_button, "Please wait, window will popup shortly", event=cevent, noti_dur=3.0, topmost=True)
- threading.Thread(target=self.check_items_func, args=(on_launch,)).start()
-
- def items_update_message(self, to_update_len):
- def main_thread():
- if show_message(f"{to_update_len} Item updates available", f"{to_update_len} Workshop Items have an update, Would you like to open the item updater window?", icon="info", _return=True):
- app.after(1, self.update_items_window)
- else:
- return
- app.after(0, main_thread)
- self.update_button.configure(state="normal", width=65, height=20)
- self.update_tooltip.configure(message='Check items for updates')
- return
-
- def check_items_func(self, on_launch):
- # Needed to refresh item that needs updates
- self.to_update.clear()
-
- def if_id_needs_update(item_id, item_date, text):
- try:
- headers = {'Cache-Control': 'no-cache'}
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={item_id}"
- response = requests.get(url, headers=headers)
- response.raise_for_status()
- content = response.text
- soup = BeautifulSoup(content, "html.parser")
- details_stats_container = soup.find("div", class_="detailsStatsContainerRight")
- details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight")
- try:
- date_updated = details_stat_elements[2].text.strip()
- except:
- try:
- date_updated = details_stat_elements[1].text.strip()
- except:
- return False
-
- if check_item_date(item_date, date_updated):
- self.to_update.add(text + f" | Updated: {date_updated}")
- return True
- else:
- return False
-
- except Exception as e:
- show_message("Error", f"Error occured\n{e}", icon="cancel")
- return
-
- def check_for_update():
- lib_data = None
-
- if not os.path.exists(os.path.join(cwd(), LIBRARY_FILE)):
- show_message("Error checking for item updates! -> Setting is on", "Please visit library tab at least once with the correct boiii path!, you also need to have at lease 1 item!")
- return
-
- with open(LIBRARY_FILE, 'r') as file:
- lib_data = json.load(file)
-
- for item in lib_data:
- item_id = item["id"]
- item_date = item["date"]
- if_id_needs_update(item_id, item_date, item["text"])
-
- check_for_update()
-
- to_update_len = len(self.to_update)
- if to_update_len > 0:
- self.items_update_message(to_update_len)
- else:
- self.update_button.configure(state="normal", width=65, height=20)
- self.update_tooltip.configure(message='Check items for updates')
- if not on_launch:
- show_message("No updates found!", "Items are up to date!", icon="info")
-
- def update_items_window(self):
- try:
- top = ctk.CTkToplevel(master=None)
- top.withdraw()
- 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("Item updater - List of Items with Updates - Click to select 1 or more")
- longest_text_length = max(len(text) for text in self.to_update)
- window_width = longest_text_length * 6 + 5
- top.geometry(f"{window_width}x450")
- top.attributes('-topmost', 'true')
- top.resizable(True, True)
- selected_id_list = []
- cevent = Event()
- self.select_all_bool = False
-
- listbox = CTkListbox(top, multiple_selection=True)
- listbox.grid(row=0, column=0, sticky="nsew")
-
- update_button = ctk.CTkButton(top, text="Update")
- update_button.grid(row=1, column=0, pady=10, padx=5, sticky='ns')
-
- select_button = ctk.CTkButton(top, text="Select All", width=5)
- select_button.grid(row=1, column=0, pady=10, padx=(230, 0), sticky='ns')
-
- def open_url(id_part, e=None):
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={id_part}"
- webbrowser.open(url)
-
- # you gotta use my modded CTkListbox originaly by Akascape
- def add_checkbox_item(index, item_text):
- parts = item_text.split('ID: ')
- id_part = parts[1].split('|')[0].strip()
- listbox.insert(index, item_text, keybind="", func=lambda e: open_url(id_part))
-
- def load_items():
- for index, item_text in enumerate(self.to_update):
- if index == len(self.to_update) - 1:
- add_checkbox_item("end", item_text)
- top.deiconify()
- return
- add_checkbox_item(index, item_text)
-
- def update_list(selected_option):
- selected_id_list.clear()
-
- if selected_option:
- for option in selected_option:
- parts = option.split('ID: ')
- if len(parts) > 1:
- id_part = parts[1].split('|')[0].strip()
- selected_id_list.append(id_part)
-
- def select_all():
- if self.select_all_bool:
- listbox.deactivate("all")
- update_list(listbox.get())
- self.select_all_bool = False
- return
- listbox.deactivate("all")
- listbox.activate("all")
- update_list(listbox.get())
- self.select_all_bool = True
-
- def update_btn_fun():
- if len(selected_id_list) == 1:
- if app.is_downloading:
- show_message("Error", "Please wait for the current download to finish or stop it then start.", icon="cancel")
- return
- app.edit_workshop_id.delete(0, "end")
- app.edit_workshop_id.insert(0, selected_id_list[0])
- app.main_button_event()
- app.download_map(update=True)
- top.destroy()
- return
-
- elif len(selected_id_list) > 1:
- if app.is_downloading:
- show_message("Error", "Please wait for the current download to finish or stop it then start.", icon="cancel")
- return
- comma_separated_ids = ",".join(selected_id_list)
- app.queuetextarea.delete("1.0", "end")
- app.queuetextarea.insert("1.0", comma_separated_ids)
- app.queue_button_event()
- app.download_map(update=True)
- top.destroy()
- return
-
- else:
- cevent.x_root = update_button.winfo_rootx()
- cevent.y_root = update_button.winfo_rooty()
- show_noti(update_button ,"Please select 1 or more items", event=cevent, noti_dur=0.8, topmost=True)
-
-
- listbox.configure(command=update_list)
- update_button.configure(command=update_btn_fun)
- select_button.configure(command=select_all)
-
- top.grid_rowconfigure(0, weight=1)
- top.grid_columnconfigure(0, weight=1)
-
- load_items()
-
- except Exception as e:
- show_message("Error", f"{e}", icon="cancel")
-
- finally:
- self.update_button.configure(state="normal", width=65, height=20)
- self.update_tooltip.configure(message='Check items for updates')
-
-class SettingsTab(ctk.CTkFrame):
- def __init__(self, master=None):
- super().__init__(master)
- # settings default bools
- self.skip_already_installed = True
- self.stopped = False
- self.console = False
- self.clean_on_finish = True
- self.continuous = True
- self.estimated_progress = True
- self.steam_fail_counter_toggle = True
- self.steam_fail_counter = 0
- self.steam_fail_number = 10
- self.steamcmd_reset = False
- self.show_fails = True
- self.check_items_on_launch = False
-
- # Left and right frames, use fg_color="transparent"
- self.grid_rowconfigure(0, weight=1)
- 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_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_columnconfigure(1, weight=1)
- self.update_idletasks()
-
- # Check for updates checkbox
- self.check_updates_var = ctk.BooleanVar()
- self.check_updates_var.trace_add("write", self.enable_save_button)
- self.check_updates_checkbox = ctk.CTkSwitch(left_frame, text="Check for updates on launch", variable=self.check_updates_var)
- self.check_updates_checkbox.grid(row=0, column=1, padx=20 , pady=(20, 0), sticky="nw")
- self.check_updates_var.set(self.load_settings("checkforupdates"))
-
- # 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.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"))
-
- # 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.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"))
-
- # clean on finish checkbox
- self.clean_checkbox_var = ctk.BooleanVar()
- self.clean_checkbox_var.trace_add("write", self.enable_save_button)
- self.clean_checkbox = ctk.CTkSwitch(left_frame, text="Clean on finish", variable=self.clean_checkbox_var)
- self.clean_checkbox.grid(row=3, column=1, padx=20, pady=(20, 0), sticky="nw")
- self.clean_checkbox_tooltip = CTkToolTip(self.clean_checkbox, message="Cleans the map that have been downloaded and installed from steamcmd's steamapps folder ,to save space")
- self.clean_checkbox_var.set(self.load_settings("clean_on_finish", "on"))
-
- # 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.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
- 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.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"))
-
- # Show skip_already_installed maps checkbox
- self.skip_already_installed_var = ctk.BooleanVar()
- self.skip_already_installed_var.trace_add("write", self.enable_save_button)
- self.skip_already_installed_ch = ctk.CTkSwitch(left_frame, text="Skip already installed maps", variable=self.skip_already_installed_var)
- self.skip_already_installed_ch.grid(row=6, column=1, padx=20, pady=(20, 0), sticky="nw")
- self.skip_already_installed_ch_tooltip = CTkToolTip(self.skip_already_installed_ch, message="If on it will not download installed maps,\nthis can miss sometimes if you remove maps manually and not from library tab while the app is running")
- self.skip_already_installed_var.set(self.load_settings("skip_already_installed", "on"))
-
- # 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.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"))
-
- # Resetr 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.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"], 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.set(value=self.load_settings("folder_naming", "PublisherID"))
-
- # Check for updates button n Launch boiii
- 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_boiii = ctk.CTkButton(right_frame, text="Launch boiii", command=self.settings_launch_boiii)
- self.launch_boiii.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_boiii = ctk.CTkButton(right_frame, text="Steam to boiii", command=self.from_steam_to_boiii_toplevel)
- self.steam_to_boiii.grid(row=5, column=1, padx=20, pady=(20, 0), sticky="n")
- self.steam_to_boiii_tooltip = CTkToolTip(self.steam_to_boiii, message="Moves/copies maps and mods from steam to boiii (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.boiiiwd_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")
-
- def reset_steamcmd_on_fail_func(self, option: str):
- if option == "Custom":
- try:
- save_config("reset_on_fail", "10")
- def callback():
- msg = CTkMessagebox(title="config.ini", message="change reset_on_fail value to whatever you want", icon="info", option_1="No", option_2="Ok", sound=True)
- response = msg.get()
- if response == "No":
- return
- elif response == "Ok":
- os.system(f"notepad {os.path.join(cwd(), 'config.ini')}")
- else:
- return
- self.after(0, callback)
- except:
- show_message("Couldn't open config.ini" ,"you can do so by yourself and change reset_on_fail value to whatever you want")
- else:
- return
- def theme_options_func(self, option: str):
- if option == "Default":
- self.boiiiwd_custom_theme(disable_only=True)
- save_config("theme", "boiiiwd_theme.json")
- if option == "Blue":
- self.boiiiwd_custom_theme(disable_only=True)
- save_config("theme", "boiiiwd_blue.json")
- if option == "Grey":
- self.boiiiwd_custom_theme(disable_only=True)
- save_config("theme", "boiiiwd_grey.json")
- if option == "Ghost":
- self.boiiiwd_custom_theme(disable_only=True)
- save_config("theme", "boiiiwd_ghost.json")
- if option == "Obsidian":
- self.boiiiwd_custom_theme(disable_only=True)
- save_config("theme", "boiiiwd_obsidian.json")
- if option == "NeonBanana":
- self.boiiiwd_custom_theme(disable_only=True)
- save_config("theme", "boiiiwd_neonbanana.json")
- if option == "Custom":
- self.boiiiwd_custom_theme()
- save_config("theme", "boiiiwd_theme.json")
- if not option == "Custom":
- if show_message("Restart to take effect!", f"{option} theme has been set ,please restart to take effect", icon="info", _return=True, option_1="Ok", option_2="Restart"):
- try:
- p = psutil.Process(os.getpid())
- for handler in p.open_files() + p.connections():
- os.close(handler.fd)
- except Exception:
- pass
- python = sys.executable
- os.execl(python, python, *sys.argv)
- else:
- pass
-
- def enable_save_button(self, *args):
- try:
- self.save_button.configure(state='normal')
- except:
- pass
-
- def save_settings(self):
- self.save_button.configure(state='disabled')
-
- if self.folder_options.get() == "PublisherID":
- save_config("folder_naming", "0")
- else:
- save_config("folder_naming", "1")
-
- if self.check_items_var.get():
- save_config("check_items", "on")
- else:
- save_config("check_items", "off")
-
- if self.check_updates_checkbox.get():
- save_config("checkforupdtes", "on")
- else:
- save_config("checkforupdtes", "off")
-
- if self.checkbox_show_console.get():
- save_config("console", "on")
- self.console = True
- else:
- save_config("console", "off")
- self.console = False
-
- if self.skip_already_installed_ch.get():
- save_config("skip_already_installed", "on")
- self.skip_already_installed = True
- else:
- save_config("skip_already_installed", "off")
- self.skip_already_installed = False
-
- if self.clean_checkbox.get():
- save_config("clean_on_finish", "on")
- self.clean_on_finish = True
- else:
- save_config("clean_on_finish", "off")
- self.clean_on_finish = False
-
- if self.checkbox_continuous.get():
- save_config("continuous_download", "on")
- self.continuous = True
- else:
- save_config("continuous_download", "off")
- self.continuous = False
-
- if self.estimated_progress_cb.get():
- save_config("estimated_progress", "on")
- self.estimated_progress = True
- else:
- save_config("estimated_progress", "off")
- self.estimated_progress = False
-
- if self.show_fails_cb.get():
- save_config("show_fails", "on")
- self.show_fails = True
- else:
- save_config("show_fails", "off")
- self.show_fails = False
-
- if self.reset_steamcmd_on_fail.get():
- value = self.reset_steamcmd_on_fail.get()
- if value == "Disable":
- self.steam_fail_counter_toggle = False
- else:
- self.steam_fail_counter_toggle = True
- self.steam_fail_number = int(value)
- save_config("reset_on_fail", value)
-
- def load_settings(self, setting, fallback=None):
- if setting == "folder_naming":
- if check_config(setting, fallback) == "1":
- return "FolderName"
- else:
- return "PublisherID"
-
- if setting == "console":
- if check_config(setting, fallback) == "on":
- self.console = True
- return 1
- else:
- self.console = False
- return 0
-
- if setting == "continuous_download":
- if check_config(setting, "on") == "on":
- self.continuous = True
- return 1
- else:
- self.continuous = False
- return 0
-
- if setting == "clean_on_finish":
- if check_config(setting, fallback) == "on":
- self.clean_on_finish = True
- return 1
- else:
- self.clean_on_finish = False
- return 0
- if setting == "estimated_progress":
- if check_config(setting, fallback) == "on":
- self.estimated_progress = True
- return 1
- else:
- self.estimated_progress = False
- return 0
-
- if setting == "reset_on_fail":
- option = check_config(setting, fallback)
- if option == "Disable" or option == "Custom":
- self.steam_fail_counter_toggle = False
- return "Disable"
- else:
- try:
- self.steam_fail_number = int(option)
- self.steam_fail_counter_toggle = True
- return option
- except:
- self.steam_fail_counter_toggle = True
- self.steam_fail_number = 10
- return "10"
-
- if setting == "show_fails":
- if check_config(setting, fallback) == "on":
- self.show_fails = True
- return 1
- else:
- self.show_fails = False
- return 0
-
- if setting == "skip_already_installed":
- if check_config(setting, fallback) == "on":
- self.skip_already_installed = True
- return 1
- else:
- self.skip_already_installed = False
- return 0
-
- if setting == "theme":
- if os.path.exists(os.path.join(cwd(), "boiiiwd_theme.json")):
- return "Custom"
- if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_theme.json":
- return "Default"
- if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_grey.json":
- return "Grey"
- if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_blue.json":
- return "Blue"
- if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_obsidian.json":
- return "Obsidian"
- if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_ghost.json":
- return "Ghost"
- if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_neonbanana.json":
- return "NeonBanana"
- else:
- if check_config(setting, fallback) == "on":
- return 1
- else:
- return 0
-
- def boiiiwd_custom_theme(self, disable_only=None):
- file_to_rename = os.path.join(cwd(), "boiiiwd_theme.json")
- if os.path.exists(file_to_rename):
- timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
- new_name = f"boiiiwd_theme_{timestamp}.json"
- os.rename(file_to_rename, os.path.join(cwd(), new_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")
- else:
- if disable_only:
- return
- try:
- shutil.copy(os.path.join(RESOURCES_DIR, check_config("theme", "boiiiwd_theme.json")), os.path.join(cwd(), "boiiiwd_theme.json"))
- except:
- shutil.copy(os.path.join(RESOURCES_DIR, "boiiiwd_theme.json"), os.path.join(cwd(), "boiiiwd_theme.json"))
- show_message("Preset file created", "You can now edit boiiiwd_theme.json in the current directory to your liking\n* Edits will apply next time you open boiiiwd\n* Program will always take boiiiwd_theme.json as the first theme option if found\n* Click on this button again to disable your custom theme or just rename boiiiwd_theme.json", icon="info")
-
- def settings_check_for_updates(self):
- check_for_updates_func(self, ignore_up_todate=False)
-
- def load_on_switch_screen(self):
- self.check_updates_var.set(self.load_settings("checkforupdtes"))
- 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"))
- self.clean_checkbox_var.set(self.load_settings("clean_on_finish", "on"))
- self.continuous_var.set(self.load_settings("continuous_download"))
- self.show_fails_var.set(self.load_settings("show_fails", "on"))
- self.skip_already_installed_var.set(self.load_settings("skip_already_installed", "on"))
-
- # keep last cuz of trace_add()
- self.save_button.configure(state='disabled')
-
- def settings_launch_boiii(self):
- launch_boiii_func(check_config("destinationfolder"))
-
- def settings_reset_steamcmd(self):
- reset_steamcmd()
-
- def from_steam_to_boiii_toplevel(self):
- def main_thread():
- try:
- 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 boiii -> Workshop items")
- top.attributes('-topmost', 'true')
- top.resizable(False, False)
- # Create input boxes
- center_frame = ctk.CTkFrame(top)
- center_frame.grid(row=0, column=0, padx=20, pady=20)
-
- # Create input boxes
- steam_folder_label = ctk.CTkLabel(center_frame, text="Steam Folder:")
- steam_folder_label.grid(row=0, column=0, padx=(20, 20), pady=(10, 0), sticky='w')
- steam_folder_entry = ctk.CTkEntry(center_frame, width=225)
- steam_folder_entry.grid(row=1, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes')
- button_steam_browse = ctk.CTkButton(center_frame, text="Select", width=10)
- button_steam_browse.grid(row=1, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes")
-
- boiii_folder_label = ctk.CTkLabel(center_frame, text="boiii Folder:")
- boiii_folder_label.grid(row=2, column=0, padx=(20, 20), pady=(10, 0), sticky='w')
- boiii_folder_entry = ctk.CTkEntry(center_frame, width=225)
- boiii_folder_entry.grid(row=3, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes')
- button_BOIII_browse = ctk.CTkButton(center_frame, text="Select", width=10)
- button_BOIII_browse.grid(row=3, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes")
-
- # Create option to choose between cut or copy
- operation_label = ctk.CTkLabel(center_frame, text="Choose operation:")
- operation_label.grid(row=4, column=0, padx=(20, 20), pady=(10, 10), sticky='wnes')
- copy_var = ctk.BooleanVar()
- cut_var = ctk.BooleanVar()
- copy_check = ctk.CTkCheckBox(center_frame, text="Copy", variable=copy_var)
- cut_check = ctk.CTkCheckBox(center_frame, text="Cut", variable=cut_var)
- copy_check.grid(row=4, column=1, padx=(0, 10), pady=(10, 10), sticky='wnes')
- cut_check.grid(row=4, column=2, padx=(0, 10), pady=(10, 10), sticky='nes')
-
- # Create progress bar
- progress_bar = ctk.CTkProgressBar(center_frame, mode="determinate", height=20, corner_radius=7)
- progress_bar.grid(row=5, column=0, columnspan=3, padx=(20, 20), pady=(10, 10), sticky='wnes')
- progress_text = ctk.CTkLabel(progress_bar, text="0%", font=("Helvetica", 12), fg_color="transparent", text_color="white", height=0, width=0, corner_radius=0)
- progress_text.place(relx=0.5, rely=0.5, anchor="center")
-
- copy_button = ctk.CTkButton(center_frame, text="Start (Copy)")
- copy_button.grid(row=6, column=0, columnspan=3,padx=(20, 20), pady=(10, 10), sticky='wnes')
-
- # funcs
- # had to use this shit again cuz of threading issues with widgets
- def copy_with_progress(src, dst):
- try:
- total_files = sum([len(files) for root, dirs, files in os.walk(src)])
- progress = 0
-
- def copy_progress(src, dst):
- nonlocal progress
- shutil.copy2(src, dst)
- progress += 1
- top.after(0, progress_text.configure(text=f"Copying files: {progress}/{total_files}"))
- value = (progress / total_files) * 100
- valuep = value / 100
- progress_bar.set(valuep)
-
- try:
- shutil.copytree(src, dst, dirs_exist_ok=True, copy_function=copy_progress)
- except Exception as E:
- show_message("Error", f"Error copying files: {E}", icon="cancel")
- finally:
- top.after(0, progress_text.configure(text="0%"))
- top.after(0, progress_bar.set(0.0))
-
- def check_status(var, op_var):
- if var.get():
- op_var.set(False)
- if cut_var.get():
- copy_button.configure(text=f"Start (Cut)")
- if copy_var.get():
- copy_button.configure(text=f"Start (Copy)")
-
- def open_BOIII_browser():
- selected_folder = ctk.filedialog.askdirectory(title="Select boiii Folder")
- if selected_folder:
- boiii_folder_entry.delete(0, "end")
- boiii_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)")
- if selected_folder:
- steam_folder_entry.delete(0, "end")
- steam_folder_entry.insert(0, selected_folder)
- save_config("steam_folder" ,steam_folder_entry.get())
-
- def start_copy_operation():
- 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!")
- return
-
- copy_button.configure(state="disabled")
- steam_folder = steam_folder_entry.get()
- ws_folder = os.path.join(steam_folder, "steamapps/workshop/content/311210")
- boiii_folder = boiii_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(boiii_folder):
- show_message("Not found", "boiii folder not found, please recheck path")
- return
-
- top.after(0, progress_text.configure(text="Loading..."))
-
- map_folder = os.path.join(ws_folder)
-
- subfolders = [f for f in os.listdir(map_folder) if os.path.isdir(os.path.join(map_folder, f))]
- total_folders = len(subfolders)
-
- if not subfolders:
- show_message("No items found", f"No items found in \n{map_folder}")
- return
-
- for i, workshop_id in enumerate(subfolders, start=1):
- json_file_path = os.path.join(map_folder, workshop_id, "workshop.json")
- copy_button.configure(text=f"Working on -> {i}/{total_folders}")
-
- if os.path.exists(json_file_path):
- mod_type = extract_json_data(json_file_path, "Type")
- items_file = os.path.join(cwd(), LIBRARY_FILE)
-
- if app.library_tab.item_exists_in_file(items_file, workshop_id):
- get_folder_name = app.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name")
- if get_folder_name:
- folder_name = get_folder_name
- else:
- try:
- folder_name = extract_json_data(json_file_path, app.settings_tab.folder_options.get())
- except:
- folder_name = extract_json_data(json_file_path, "publisherID")
- else:
- try:
- folder_name = extract_json_data(json_file_path, app.settings_tab.folder_options.get())
- except:
- folder_name = extract_json_data(json_file_path, "publisherID")
-
- if mod_type == "mod":
- mods_folder = os.path.join(boiii_folder, "mods")
- folder_name_path = os.path.join(mods_folder, folder_name, "zone")
- elif mod_type == "map":
- usermaps_folder = os.path.join(boiii_folder, "usermaps")
- folder_name_path = os.path.join(usermaps_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")
- continue
-
- os.makedirs(folder_name_path, exist_ok=True)
-
- try:
- copy_with_progress(os.path.join(map_folder, workshop_id), folder_name_path)
- except Exception as E:
- show_message("Error", f"Error copying files: {E}", icon="cancel")
- continue
-
- if cut_var.get():
- remove_tree(os.path.join(map_folder, workshop_id))
-
- if subfolders:
- 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:
- if cut_var.get():
- copy_button.configure(text=f"Start (Cut)")
- if copy_var.get():
- copy_button.configure(text=f"Start (Copy)")
- copy_button.configure(state="normal")
- top.after(0, progress_bar.set(0))
- top.after(0, progress_text.configure(text="0%"))
-
- # prevents app hanging
- threading.Thread(target=start_thread).start()
-
- # config
- progress_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "progress_bar_fill_color")
- progress_bar.configure(progress_color=progress_color)
- steam_folder_entry.insert(1, check_config("steam_folder", ""))
- boiii_folder_entry.insert(1, app.edit_destination_folder.get())
- button_BOIII_browse.configure(command=open_BOIII_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))
- app.create_context_menu(steam_folder_entry)
- app.create_context_menu(boiii_folder_entry)
- copy_var.set(True)
- progress_bar.set(0)
-
- except Exception as e:
- show_message("Error", f"{e}", icon="cancel")
-
- app.after(0, main_thread)
-
-class BOIIIWD(ctk.CTk):
- def __init__(self):
- super().__init__()
- # self.app_instance = BOIIIWD()
-
- # configure window
- self.title("boiii Workshop Downloader - Main")
-
- try:
- geometry_file = os.path.join(cwd(), "boiiiwd_dont_touch.conf")
- if os.path.isfile(geometry_file):
- with open(geometry_file, "r") as conf:
- self.geometry(conf.read())
- else:
- self.geometry(f"{910}x{560}")
- except:
- self.geometry(f"{910}x{560}")
-
- 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)
-
- 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.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.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.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.skip_boutton = ctk.CTkButton(master=self.qeueuframe, text="Skip", command=self.skip_current_queue_item, width=10, height=10, fg_color="#585858")
-
- self.qeueuframe.grid_remove()
-
- # configure grid layout (4x4)
- self.grid_columnconfigure(1, weight=1)
- self.grid_columnconfigure((2, 3), weight=0)
- self.grid_rowconfigure((0, 1, 2), weight=1)
- self.settings_tab = SettingsTab(self)
- self.library_tab = LibraryTab(self, corner_radius=3)
-
- # create sidebar frame with widgets
- font = "Comic Sans MS"
- if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.png")):
- ryuks_icon = os.path.join(RESOURCES_DIR, "ryuk.png")
- self.sidebar_icon = ctk.CTkImage(light_image=Image.open(ryuks_icon), dark_image=Image.open(ryuks_icon), size=(40, 40))
- else:
- self.sidebar_icon = None
- self.sidebar_frame = ctk.CTkFrame(self, width=100, corner_radius=10)
- self.sidebar_frame.grid(row=0, column=0, rowspan=3, padx=(10, 20), pady=(10, 10), sticky="nsew")
- self.sidebar_frame.grid_rowconfigure(4, weight=1)
- self.logo_label = ctk.CTkLabel(self.sidebar_frame, text='',image=self.sidebar_icon)
- self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
- self.txt_label = ctk.CTkLabel(self.sidebar_frame, text="- Sidebar -", font=(font, 17))
- 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)
- 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.sidebar_settings = ctk.CTkButton(self.sidebar_frame, height=28)
- self.sidebar_settings.grid(row=5, 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.txt_main = ctk.CTkLabel(self.optionsframe, text="💎 BOIIIWD 💎", font=(font, 20))
- self.txt_main.grid(row=0, column=1, columnspan=5, padx=0, pady=(20, 20), sticky="n")
-
- # create slider and progressbar frame
- self.slider_progressbar_frame = ctk.CTkFrame(self)
- self.slider_progressbar_frame.grid(row=2, column=1, rowspan=1, padx=(0, 20), pady=(20, 20), sticky="nsew")
-
- self.slider_progressbar_frame.columnconfigure(0, weight=0)
- self.slider_progressbar_frame.columnconfigure(1, weight=1)
- self.slider_progressbar_frame.columnconfigure(2, weight=0)
- self.slider_progressbar_frame.rowconfigure(0, weight=1)
- self.slider_progressbar_frame.rowconfigure(1, weight=1)
- 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")
- self.elapsed_time.grid(row=1, column=1, padx=20, pady=(0, 10), sticky="nsew") # Use "nsew" to center label
- self.label_file_size = ctk.CTkLabel(master=self.slider_progressbar_frame, text="File size: 0KB")
- self.label_file_size.grid(row=1, column=2, padx=(0, 20), pady=(0, 10), sticky="e")
-
- self.progress_bar = ctk.CTkProgressBar(master=self.slider_progressbar_frame, mode="determinate", height=20, corner_radius=7)
- self.progress_bar.grid(row=2, column=0, padx=20, pady=(0, 10), columnspan=3, sticky="ew")
-
- self.progress_text = ctk.CTkLabel(self.progress_bar, text="0%", font=("Helvetica", 12), fg_color="transparent", text_color="white", height=0, width=0, corner_radius=0)
- self.progress_text.place(relx=0.5, rely=0.5, anchor="center")
-
- self.button_download = ctk.CTkButton(master=self.slider_progressbar_frame, text="Download", command=self.download_map)
- self.button_download.grid(row=4, column=0, padx=20, pady=(5, 20), columnspan=2, sticky="ew")
-
- self.button_stop = ctk.CTkButton(master=self.slider_progressbar_frame, text="Stop", command=self.stop_download)
- self.button_stop.grid(row=4, column=2, padx=(0, 20), pady=(5, 20), columnspan=1, sticky="w")
-
- # 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.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.check_if_changed = ctk.StringVar()
- self.check_if_changed.trace_add("write", self.id_chnaged_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.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 boiii 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 boiii 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 boiii Instalation folder")
- self.edit_destination_folder.grid(row=4, column=1, padx=20, pady=(0, 25), columnspan=4, sticky="ewn")
-
- self.button_BOIII_browse = ctk.CTkButton(master=self.optionsframe, text="Select", command=self.open_BOIII_browser)
- self.button_BOIII_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")
-
-
- # set default values
- self.active_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "button_active_state_color")
- self.normal_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "button_normal_state_color")
- self.progress_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "progress_bar_fill_color")
- self.settings_tab.appearance_mode_optionemenu.set("Dark")
- self.settings_tab.scaling_optionemenu.set("100%")
- 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.item_skipped = False
- self.fail_threshold = 0
-
- # sidebar windows bouttons
- 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)
- 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")
- self.bind("", self.save_window_size)
-
- # 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.queuetextarea, textbox=True)
- self.create_context_menu(self.library_tab.filter_entry, textbox=False, library=True)
- # valid event required for filter_items()
- self.cevent = Event()
- self.cevent.x = 0
- self.cevent.y = 0
-
- # load ui configs
- self.load_configs()
-
- if check_config("checkforupdtes") == "on":
- self.withdraw()
- check_for_updates_func(self, ignore_up_todate=True)
- self.update()
- self.deiconify()
-
- try:
- self.settings_tab.load_settings("clean_on_finish", "on")
- self.settings_tab.load_settings("continuous_download", "on")
- self.settings_tab.load_settings("console", "off")
- self.settings_tab.load_settings("estimated_progress", "on")
- self.settings_tab.load_settings("reset_on_fail", "10")
- self.settings_tab.load_settings("show_fails", "on")
- self.settings_tab.load_settings("skip_already_installed", "on")
- except:
- pass
-
- if not check_steamcmd():
- self.show_steam_warning_message()
-
- # items check for update, ill change all the variables to work this way at a later date
- if self.settings_tab.check_items_var.get():
- self.library_tab.check_for_updates(on_launch=True)
-
- def do_popup(self, event, frame):
- try: frame.tk_popup(event.x_root, event.y_root)
- finally: frame.grab_release()
-
- def create_context_menu(self, text_widget, textbox=False, library=False):
- context_menu = Menu(text_widget, tearoff=False, background='#565b5e', fg='white', borderwidth=0, bd=0)
- context_menu.add_command(label="Paste", command=lambda: self.clipboard_paste(text_widget, textbox, library))
- context_menu.add_separator()
- context_menu.add_command(label="Copy", command=lambda: self.clipboard_copy(text_widget, textbox, library))
- context_menu.add_separator()
- context_menu.add_command(label="Cut", command=lambda: self.clipboard_cut(text_widget, textbox, library))
- context_menu.add_separator()
- context_menu.add_command(label="Select All", command=lambda: self.select_all(text_widget, textbox))
- text_widget.bind("", lambda event: self.do_popup(event, frame=context_menu))
-
- def clipboard_copy(self, text, textbox=False, library=False):
- text.clipboard_clear()
- try:
- text.clipboard_append(text.selection_get())
- except:
- if textbox:
- text.clipboard_append(text.get("1.0", END))
- else:
- text.clipboard_append(text.get())
- finally:
- if library:
- self.library_tab.filter_items(self.cevent)
-
- def clipboard_paste(self, text, textbox=False, library=False):
- try:
- if textbox:
- text_cont = text.get("1.0", END)
- else:
- text_cont = text.get()
- if textbox:
- if text.tag_ranges("sel"):
- text.delete("sel.first", "sel.last")
- else:
- if text.selection_get() in text_cont:
- start_index = text_cont.index(text.selection_get())
- end_index = start_index + len(text.selection_get())
- text.delete(start_index, end_index)
- text.insert(ctk.INSERT, text.clipboard_get())
- except:
- text.insert(ctk.INSERT, text.clipboard_get())
- finally:
- if library:
- self.library_tab.filter_items(self.cevent)
-
- def select_all(self, text_widget, textbox=False):
- if textbox:
- text_widget.tag_add("sel", "1.0", "end")
- text_widget.focus()
- else:
- text_widget.select_range(0, END)
- text_widget.focus()
-
- def clipboard_cut(self, text, textbox=False, library=False):
- text.clipboard_clear()
- if textbox:
- text_cont = text.get(1.0, END)
- else:
- text_cont = text.get()
- try:
- if textbox:
- if text.tag_ranges("sel"):
- selected_text = text.get("sel.first", "sel.last")
- text.clipboard_append(selected_text)
- text.delete("sel.first", "sel.last")
- else:
- raise
- else:
- text.clipboard_append(text.selection_get())
- if text.selection_get() in text_cont:
- start_index = text_cont.index(text.selection_get())
- end_index = start_index + len(text.selection_get())
- text.delete(start_index, end_index)
- except:
- if textbox:
- text.clipboard_append(text.get("1.0", END))
- text.delete(1.0, "end")
- else:
- text.clipboard_append(text.get())
- text.delete(0, "end")
- finally:
- if library:
- self.library_tab.filter_items(self.cevent)
-
- def save_window_size(self, event):
- with open("boiiiwd_dont_touch.conf", "w") as conf:
- conf.write(self.geometry())
-
- def on_closing(self):
- save_config("DestinationFolder" ,self.edit_destination_folder.get())
- save_config("SteamCMDPath" ,self.edit_steamcmd_path.get())
- self.stop_download(on_close=True)
- os._exit(0)
-
- def id_chnaged_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):
- check_for_updates_func(self, ignore_up_todate=False)
-
- def change_appearance_mode_event(self, new_appearance_mode: str):
- ctk.set_appearance_mode(new_appearance_mode)
- save_config("appearance", new_appearance_mode)
-
- def change_scaling_event(self, new_scaling: str):
- new_scaling_float = int(new_scaling.replace("%", "")) / 100
- ctk.set_widget_scaling(new_scaling_float)
- save_config("scaling", str(new_scaling_float))
-
- def hide_main_widgets(self):
- self.optionsframe.grid_forget()
- self.slider_progressbar_frame.grid_forget()
-
- def show_main_widgets(self):
- self.title("BOIII 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")
-
- def hide_settings_widgets(self):
- self.settings_tab.grid_forget()
-
- def show_settings_widgets(self):
- self.title("BOIII Workshop Downloader - Settings")
- self.settings_tab.grid(row=0, rowspan=3, column=1, padx=(0, 20), pady=(20, 20), sticky="nsew")
- self.settings_tab.load_on_switch_screen()
-
- def hide_library_widgets(self):
- self.library_tab.grid_remove()
-
- def show_library_widgets(self):
- self.title("BOIII Workshop Downloader - Library")
- status = self.library_tab.load_items(self.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"BOIII Workshop Downloader - Library ➜ {status}")
-
- def show_queue_widgets(self):
- self.title("BOIII 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")
-
- def hide_queue_widgets(self):
- self.queue_enabled = False
- self.qeueuframe.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))
- self.hide_settings_widgets()
- self.hide_library_widgets()
- self.hide_queue_widgets()
- self.show_main_widgets()
-
- 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))
- self.sidebar_settings.configure(state="active", fg_color=(self.active_color))
- self.hide_main_widgets()
- self.hide_library_widgets()
- self.hide_queue_widgets()
- self.show_settings_widgets()
-
- 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))
- self.sidebar_library.configure(state="active", fg_color=(self.active_color))
- self.hide_main_widgets()
- self.hide_settings_widgets()
- self.hide_queue_widgets()
- self.show_library_widgets()
-
- def queue_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_library.configure(state="normal", fg_color=(self.normal_color))
- self.sidebar_queue.configure(state="active", fg_color=(self.active_color))
- self.hide_settings_widgets()
- self.hide_library_widgets()
- self.show_queue_widgets()
-
- def load_configs(self):
- if os.path.exists(CONFIG_FILE_PATH):
- destination_folder = check_config("DestinationFolder", "")
- steamcmd_path = check_config("SteamCMDPath", os.getcwd())
- 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)
- ctk.set_appearance_mode(new_appearance_mode)
- ctk.set_widget_scaling(float(new_scaling))
- self.settings_tab.appearance_mode_optionemenu.set(new_appearance_mode)
- scaling_float = float(new_scaling)*100
- scaling_int = math.trunc(scaling_float)
- self.settings_tab.scaling_optionemenu.set(f"{scaling_int}%")
- else:
- new_appearance_mode = check_config("appearance", "Dark")
- new_scaling = check_config("scaling", 1.0)
- ctk.set_appearance_mode(new_appearance_mode)
- ctk.set_widget_scaling(float(new_scaling))
- self.settings_tab.appearance_mode_optionemenu.set(new_appearance_mode)
- 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, cwd())
- 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..."
- 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.help_button.configure(text="Help")
- self.queuetextarea.configure(state="normal")
- self.queuetextarea.delete(1.0, "end")
- self.queuetextarea.insert(1.0, "")
- if self.help_restore_content:
- self.queuetextarea.insert(1.0, self.help_restore_content.strip())
- else:
- 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_button.configure(text="Restore")
- self.queuetextarea.configure(state="normal")
- 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.queuetextarea.insert(1.0, help_text)
- self.queuetextarea.configure(state="disabled")
- else:
- self.help_restore_content = textarea_content
- self.workshop_queue_label.configure(text="Workshop IDs/Links => press restore to remove examples:")
- self.help_button.configure(text="Restore")
- self.queuetextarea.insert(1.0, help_text)
- self.queuetextarea.configure(state="disabled")
-
- def open_BOIII_browser(self):
- selected_folder = ctk.filedialog.askdirectory(title="Select boiii 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 show_steam_warning_message(self):
- def callback():
- msg = CTkMessagebox(title="Warning", message="steamcmd.exe was not found in the specified directory.\nPress Download to get it or Press Cancel and select it from there!.",
- icon="warning", option_1="Cancel", option_2="Download", sound=True)
- response = msg.get()
- if response == "Cancel":
- return
- elif response == "Download":
- self.download_steamcmd()
- self.after(0, callback)
-
- def open_browser(self):
- link = "https://steamcommunity.com/app/311210/workshop/"
- webbrowser.open(link)
-
- def download_steamcmd(self):
- self.edit_steamcmd_path.delete(0, "end")
- self.edit_steamcmd_path.insert(0, cwd())
- save_config("DestinationFolder" ,self.edit_destination_folder.get())
- save_config("SteamCMDPath" ,self.edit_steamcmd_path.get())
- steamcmd_url = "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"
- steamcmd_zip_path = os.path.join(cwd(), "steamcmd.zip")
-
- try:
- response = requests.get(steamcmd_url)
- response.raise_for_status()
-
- with open(steamcmd_zip_path, "wb") as zip_file:
- zip_file.write(response.content)
-
- with zipfile.ZipFile(steamcmd_zip_path, "r") as zip_ref:
- zip_ref.extractall(cwd())
-
- if check_steamcmd():
- os.remove(fr"{steamcmd_zip_path}")
- def inti_steam():
- msg = CTkMessagebox(title="Success", message="SteamCMD has been downloaded ,Press ok to initialize it.", icon="info", option_1="No", option_2="Ok", sound=True)
- response = msg.get()
- if response == "No":
- pass
- elif response == "Ok":
- initialize_steam_thread = threading.Thread(target=lambda: initialize_steam(self))
- initialize_steam_thread.start()
- else:
- pass
- self.after(0, inti_steam)
- else:
- show_message("Error", "Failed to find steamcmd.exe after extraction.\nMake you sure to select the correct SteamCMD path (by default current BOIIIWD path)", icon="cancel")
- os.remove(fr"{steamcmd_zip_path}")
- except requests.exceptions.RequestException as e:
- show_message("Error", f"Failed to download SteamCMD: {e}", icon="cancel")
- os.remove(fr"{steamcmd_zip_path}")
- except zipfile.BadZipFile:
- show_message("Error", "Failed to extract SteamCMD. The downloaded file might be corrupted.", icon="cancel")
- os.remove(fr"{steamcmd_zip_path}")
-
- def show_map_info(self):
- def show_map_thread():
- workshop_id = self.edit_workshop_id.get().strip()
-
- if not workshop_id:
- show_message("Warning", "Please enter a Workshop ID/Link first.")
- return
-
- if not workshop_id.isdigit():
- try:
- if extract_workshop_id(workshop_id).strip().isdigit():
- workshop_id = extract_workshop_id(workshop_id).strip()
- else:
- show_message("Warning", "Please enter a valid Workshop ID/Link.")
- except:
- show_message("Warning", "Please enter a valid Workshop ID/Link.")
- return
- if self.button_download._state == "normal":
- self.after(1, lambda mid=workshop_id: self.label_file_size.configure(text=f"File size: {get_workshop_file_size(mid ,raw=True)}"))
-
- try:
- headers = {'Cache-Control': 'no-cache'}
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
- response = requests.get(url, headers=headers)
- response.raise_for_status()
- content = response.text
-
- soup = BeautifulSoup(content, "html.parser")
-
- try:
- map_mod_type = soup.find("div", class_="rightDetailsBlock").text.strip()
- map_name = soup.find("div", class_="workshopItemTitle").text.strip()
- map_size = map_size = get_workshop_file_size(workshop_id, raw=True)
- details_stats_container = soup.find("div", class_="detailsStatsContainerRight")
- details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight")
- date_created = details_stat_elements[1].text.strip()
- try:
- ratings = soup.find('div', class_='numRatings')
- ratings_text = ratings.get_text()
- except:
- ratings = "Not found"
- ratings_text= "Not enough ratings"
- try:
- date_updated = details_stat_elements[2].text.strip()
- except:
- date_updated = "Not updated"
- stars_div = soup.find("div", class_="fileRatingDetails")
- starts = stars_div.find("img")["src"]
- except:
- show_message("Warning", "Please enter a valid Workshop ID/Link\nCouldn't get information.")
- return
-
- try:
- preview_image_element = soup.find("img", id="previewImage")
- workshop_item_image_url = preview_image_element["src"]
- except:
- try:
- preview_image_element = soup.find("img", id="previewImageMain")
- workshop_item_image_url = preview_image_element["src"]
- except Exception as e:
- show_message("Warning", f"Failed to get preview image ,probably wrong link/id if not please open an issue on github.\n{e}")
- return
-
- starts_image_response = requests.get(starts)
- stars_image = Image.open(io.BytesIO(starts_image_response.content))
- stars_image_size = stars_image.size
-
- image_response = requests.get(workshop_item_image_url)
- image_response.raise_for_status()
- image = Image.open(io.BytesIO(image_response.content))
- image_size = image.size
-
- self.toplevel_info_window(map_name, map_mod_type, map_size, image, image_size, date_created ,
- date_updated, stars_image, stars_image_size, ratings_text, url)
-
- except requests.exceptions.RequestException as e:
- show_message("Error", f"Failed to fetch map information.\nError: {e}", icon="cancel")
- return
-
- info_thread = threading.Thread(target=show_map_thread)
- info_thread.start()
-
- def toplevel_info_window(self, map_name, map_mod_type, map_size, image, image_size,
- date_created ,date_updated, stars_image, stars_image_size, ratings_text, url):
- def main_thread():
- top = ctk.CTkToplevel(self)
- top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")))
- top.title("Map/Mod Information")
- top.attributes('-topmost', 'true')
-
- def close_window():
- top.destroy()
-
- def view_map_mod():
- webbrowser.open(url)
-
- # frames
- stars_frame = ctk.CTkFrame(top)
- stars_frame.grid(row=0, column=0, columnspan=2, padx=20, pady=(20, 0), sticky="nsew")
- stars_frame.columnconfigure(0, weight=0)
- stars_frame.rowconfigure(0, weight=1)
-
- image_frame = ctk.CTkFrame(top)
- image_frame.grid(row=1, column=0, columnspan=2, padx=20, pady=0, sticky="nsew")
-
- info_frame = ctk.CTkFrame(top)
- info_frame.grid(row=2, column=0, columnspan=2, padx=20, pady=20, sticky="nsew")
-
- buttons_frame = ctk.CTkFrame(top)
- buttons_frame.grid(row=3, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="nsew")
-
- # fillers
- name_label = ctk.CTkLabel(info_frame, text=f"Name: {map_name}")
- name_label.grid(row=0, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- type_label = ctk.CTkLabel(info_frame, text=f"Type: {map_mod_type}")
- type_label.grid(row=1, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- size_label = ctk.CTkLabel(info_frame, text=f"Size: {map_size}")
- size_label.grid(row=2, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- date_created_label = ctk.CTkLabel(info_frame, text=f"Posted: {date_created}")
- date_created_label.grid(row=3, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated}")
- date_updated_label.grid(row=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
-
- stars_image_label = ctk.CTkLabel(stars_frame)
- stars_width, stars_height = stars_image_size
- stars_image_widget = ctk.CTkImage(stars_image, size=(int(stars_width), int(stars_height)))
- stars_image_label.configure(image=stars_image_widget, text="")
- stars_image_label.pack(side="left", padx=(10, 20), pady=(10, 10))
-
- ratings = ctk.CTkLabel(stars_frame)
- ratings.configure(text=ratings_text)
- ratings.pack(side="right", padx=(10, 20), pady=(10, 10))
-
- image_label = ctk.CTkLabel(image_frame)
- width, height = image_size
- image_widget = ctk.CTkImage(image, size=(int(width), int(height)))
- image_label.configure(image=image_widget, text="")
- image_label.pack(expand=True, fill="both", padx=(10, 20), pady=(10, 10))
-
- # Buttons
- close_button = ctk.CTkButton(buttons_frame, text="View", command=view_map_mod)
- close_button.pack(side="left", padx=(10, 20), pady=(10, 10))
-
- view_button = ctk.CTkButton(buttons_frame, text="Close", command=close_window)
- view_button.pack(side="right", padx=(10, 20), pady=(10, 10))
-
- top.grid_rowconfigure(0, weight=0)
- top.grid_rowconfigure(1, weight=0)
- top.grid_rowconfigure(2, weight=1)
- top.grid_columnconfigure(0, weight=1)
- top.grid_columnconfigure(1, weight=1)
-
- self.after(0, main_thread)
-
- def check_steamcmd_stdout(self, log_file_path, target_item_id):
- temp_file_path = log_file_path + '.temp'
- shutil.copy2(log_file_path, temp_file_path)
-
- 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 f"download item {target_item_id.strip()}" in line:
- return True
-
- return False
- finally:
- os.remove(temp_file_path)
-
- def skip_current_queue_item(self):
- if self.button_download._state == "normal":
- self.skip_boutton.grid_remove()
- self.after(1, self.status_text.configure(text=f"Status: Standby!"))
- return
- self.settings_tab.stopped = True
- self.item_skipped = True
- self.settings_tab.steam_fail_counter = 0
- self.is_pressed = False
- self.is_downloading = False
- self.after(1, self.label_file_size.configure(text=f"File size: 0KB"))
-
- subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- creationflags=subprocess.CREATE_NO_WINDOW)
- self.skip_boutton.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%")
- self.progress_bar.set(0.0)
-
- # the real deal
- def run_steamcmd_command(self, command, map_folder, wsid, queue=None):
- steamcmd_path = get_steamcmd_path()
- stdout_path = os.path.join(steamcmd_path, "logs", "workshop_log.txt")
- timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
-
- os.makedirs(os.path.dirname(stdout_path), exist_ok=True)
-
- try:
- with open(stdout_path, 'w') as file:
- file.write('')
- except:
- os.rename(stdout_path, os.path.join(map_folder, os.path.join(stdout_path, f"workshop_log_couldntremove_{timestamp}.txt")))
-
- show_console = subprocess.CREATE_NO_WINDOW
- if self.settings_tab.console:
- show_console = subprocess.CREATE_NEW_CONSOLE
-
- if os.path.exists(map_folder):
- try:
- try:
- os.remove(map_folder)
- except:
- os.rename(map_folder, os.path.join(map_folder, os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", f"couldntremove_{timestamp}")))
- except Exception as e:
- self.settings_tab.stopped = True
- self.queue_stop_button = True
- show_message("Error", f"Couldn't remove {map_folder}, please do so manually\n{e}", icon="cancel")
- self.stop_download()
- return
-
- if self.settings_tab.continuous:
- start_time = 0
- while not os.path.exists(map_folder) and not self.settings_tab.stopped:
- process = subprocess.Popen(
- [steamcmd_path + "\steamcmd.exe"] + command.split(),
- stdout=None if self.settings_tab.console else subprocess.PIPE,
- stderr=None if self.settings_tab.console else subprocess.PIPE,
- text=True,
- bufsize=1,
- universal_newlines=True,
- creationflags=show_console
- )
-
- #wait for process
- while True:
- if not self.is_downloading:
- if self.check_steamcmd_stdout(stdout_path, wsid):
- start_time = time.time()
- self.is_downloading = True
- elapsed_time = time.time() - start_time
- if process.poll() != None:
- break
- time.sleep(1)
-
- # print("Broken freeeee!")
- self.is_downloading = False
- try:
- with open(stdout_path, 'w') as file:
- file.write('')
- except:
- os.rename(stdout_path, os.path.join(map_folder, os.path.join(stdout_path, f"workshop_log_couldntremove_{timestamp}.txt")))
-
- if not self.settings_tab.stopped:
- self.settings_tab.steam_fail_counter = self.settings_tab.steam_fail_counter + 1
- if elapsed_time < 20 and elapsed_time > 0 and not os.path.exists(map_folder):
- self.fail_threshold = self.fail_threshold + 1
-
- if self.settings_tab.steam_fail_counter_toggle:
- try:
- if self.fail_threshold >= int(self.settings_tab.steam_fail_number):
- reset_steamcmd(no_warn=True)
- self.settings_tab.steamcmd_reset = True
- self.settings_tab.steam_fail_counter = 0
- self.fail_threshold = 0
- except:
- if self.fail_threshold >= 25:
- reset_steamcmd(no_warn=True)
- self.settings_tab.steam_fail_counter = 0
- self.fail_threshold = 0
- else:
- process = subprocess.Popen(
- [steamcmd_path + "\steamcmd.exe"] + command.split(),
- stdout=None if self.settings_tab.console else subprocess.PIPE,
- stderr=None if self.settings_tab.console else subprocess.PIPE,
- text=True,
- bufsize=1,
- universal_newlines=True,
- creationflags=show_console
- )
-
- while True:
- if not self.is_downloading:
- if self.check_steamcmd_stdout(stdout_path, wsid):
- self.is_downloading = True
- if process.poll() != None:
- break
- time.sleep(1)
-
- # print("Broken freeeee!")
- self.is_downloading = False
- try:
- with open(stdout_path, 'w') as file:
- file.write('')
- except:
- os.rename(stdout_path, os.path.join(map_folder, os.path.join(stdout_path, f"workshop_log_couldntremove_{timestamp}.txt")))
-
- if not os.path.exists(map_folder):
- show_message("SteamCMD has terminated", "SteamCMD has been terminated\nAnd failed to download the map/mod, try again or enable continuous download in settings")
-
- self.settings_tab.stopped = True
- if not queue:
- self.button_download.configure(state="normal")
- self.button_stop.configure(state="disabled")
-
- return process.returncode
-
- def show_init_message(self):
- def callback():
- msg = CTkMessagebox(title="Warning", message="SteamCMD is not initialized, Press OK to do so!\nProgram may go unresponsive until SteamCMD is finished downloading.", icon="info", option_1="No", option_2="Ok", sound=True)
- response = msg.get()
- if response == "No":
- return
- elif response == "Ok":
- initialize_steam_thread = threading.Thread(target=lambda: initialize_steam(self))
- initialize_steam_thread.start()
- else:
- return
- self.after(0, callback)
-
- def show_complete_message(self, message):
- def callback():
- msg = CTkMessagebox(title="Downloads Complete", message=message, icon="info", option_1="Launch", option_2="Ok", sound=True)
- response = msg.get()
- if response=="Launch":
- launch_boiii_func(self.edit_destination_folder.get().strip())
- if response=="Ok":
- return
- self.after(0, callback)
-
- def download_map(self, update=False):
- self.is_downloading = False
- self.fail_threshold = 0
- 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())
- if self.queue_enabled:
- self.item_skipped = False
- start_down_thread = threading.Thread(target=self.queue_download_thread, args=(update,))
- start_down_thread.start()
- else:
- start_down_thread = threading.Thread(target=self.download_thread, args=(update,))
- start_down_thread.start()
- else:
- show_message("Warning", "Already pressed, Please wait.")
-
- def queue_download_thread(self, update=None):
- 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())
-
- if not check_steamcmd():
- self.show_steam_warning_message()
- return
-
- steamcmd_path = get_steamcmd_path()
-
- if not is_steamcmd_initialized():
- self.show_init_message()
- return
-
- text = self.queuetextarea.get("1.0", "end")
- items = []
- if "," in text:
- items = [n.strip() for n in text.split(",")]
- else:
- items = [n.strip() for n in text.split("\n") if n.strip()]
-
- if not items:
- show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
- self.stop_download()
- return
-
- destination_folder = self.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!.")
- self.stop_download()
- return
-
- if not steamcmd_path or not os.path.exists(steamcmd_path):
- show_message("Error", "Please enter a valid SteamCMD path => in the main tab!.")
- self.stop_download()
- return
-
- self.total_queue_size = 0
- self.already_installed = []
- for item in items:
- self.fail_threshold = 0
- item.strip()
- workshop_id = item
- if not workshop_id.isdigit():
- try:
- if extract_workshop_id(workshop_id).strip().isdigit():
- workshop_id = extract_workshop_id(workshop_id).strip()
- else:
- show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
- self.stop_download()
- return
- except:
- show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
- self.stop_download()
- return
- if not valid_id(workshop_id):
- show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
- self.stop_download()
- return
-
- ws_file_size = get_workshop_file_size(workshop_id)
- file_size = 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
-
- if any(workshop_id in item for item in self.library_tab.added_items):
- self.already_installed.append(workshop_id)
-
- if not update:
- if self.already_installed:
- item_ids = ", ".join(self.already_installed)
- if self.settings_tab.skip_already_installed:
- for item in self.already_installed:
- if item in items:
- items.remove(item)
- show_message("Heads up!, map/s skipped => skip is on in settings", f"These item IDs may already be installed and are skipped:\n{item_ids}", icon="info")
- if not any(isinstance(item, int) for item in items):
- self.stop_download()
- return
- else:
- show_message("Heads up! map/s not skipped => skip is off in settings", f"These item IDs may already be installed:\n{item_ids}", icon="info")
-
- self.after(1, self.status_text.configure(text=f"Status: Total size: ~{convert_bytes_to_readable(self.total_queue_size)}"))
- start_time = time.time()
- for index, item in enumerate(items):
- self.settings_tab.steam_fail_counter = 0
- current_number = index + 1
- total_items = len(items)
- if self.queue_stop_button:
- self.stop_download()
- break
- item.strip()
- self.settings_tab.stopped = False
- workshop_id = item
- if not workshop_id.isdigit():
- try:
- if extract_workshop_id(workshop_id).strip().isdigit():
- workshop_id = extract_workshop_id(workshop_id).strip()
- else:
- show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
- self.stop_download()
- return
- except:
- show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
- self.stop_download()
- return
- ws_file_size = get_workshop_file_size(workshop_id)
- file_size = ws_file_size
- 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)
- if not os.path.exists(download_folder):
- os.makedirs(download_folder)
-
- def check_and_update_progress():
- previous_net_speed = 0
- est_downloaded_bytes = 0
- file_size = ws_file_size
- item_name = get_item_name(workshop_id) if get_item_name(workshop_id) else "Error getting name"
-
- while not self.settings_tab.stopped:
- if self.settings_tab.steamcmd_reset:
- self.settings_tab.steamcmd_reset = False
- previous_net_speed = 0
- est_downloaded_bytes = 0
-
- if self.item_skipped:
- if index > 0:
- prev_item_size = None
- previous_item = items[index - 1]
- prev_item_path = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "downloads", "311210", previous_item)
- prev_item_path_2 = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", previous_item)
- if os.path.exists(prev_item_path):
- prev_item_size = sum(os.path.getsize(os.path.join(prev_item_path, f)) for f in os.listdir(prev_item_path))
- elif os.path.exists(prev_item_path_2):
- prev_item_size = sum(os.path.getsize(os.path.join(prev_item_path_2, f)) for f in os.listdir(prev_item_path_2))
- else:
- prev_item_size = get_workshop_file_size(previous_item)
- if prev_item_size:
- self.total_queue_size -= prev_item_size
- 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..."))
- time_elapsed = time.time() - start_time
- elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed)
- if self.settings_tab.show_fails:
- 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} - Fails: {self.fail_threshold}"))
- else:
- 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}"))
- 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")
- if index == len(items) - 1:
- self.skip_boutton.grid_remove()
- time.sleep(1)
- if self.is_downloading:
- break
-
- try:
- current_size = sum(os.path.getsize(os.path.join(download_folder, f)) for f in os.listdir(download_folder))
- except:
- try:
- current_size = sum(os.path.getsize(os.path.join(map_folder, f)) for f in os.listdir(map_folder))
- except:
- continue
-
- progress = int(current_size / file_size * 100)
-
- if progress > 100 and not self.settings_tab.stopped:
- progress = int(current_size / current_size * 100)
- self.total_queue_size -= file_size
- file_size = current_size
- self.total_queue_size += file_size
- 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} | Downloading {current_number}/{total_items}"))
- self.after(1, lambda p=progress: self.label_file_size.configure(text=f"Wrong size reported\nFile size: ~{convert_bytes_to_readable(current_size)}"))
-
- if self.settings_tab.estimated_progress and not self.settings_tab.stopped:
- time_elapsed = time.time() - start_time
- raw_net_speed = psutil.net_io_counters().bytes_recv
-
- current_net_speed_text = raw_net_speed
- net_speed_bytes = current_net_speed_text - previous_net_speed
- previous_net_speed = current_net_speed_text
-
- current_net_speed = net_speed_bytes
- down_cap = 150000000
- if current_net_speed >= down_cap:
- current_net_speed = 10
-
- est_downloaded_bytes += current_net_speed
-
- percentage_complete = (est_downloaded_bytes / file_size) * 100
-
- progress = min(percentage_complete / 100, 0.99)
-
- net_speed, speed_unit = convert_speed(net_speed_bytes)
-
- elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed)
-
- # print(f"raw_net {raw_net_speed}\ncurrent_net_speed: {current_net_speed}\nest_downloaded_bytes {est_downloaded_bytes}\npercentage_complete {percentage_complete}\nprogress {progress}")
- 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} | Downloading {current_number}/{total_items}"))
- self.after(1, self.progress_bar.set(progress))
- self.after(1, lambda v=net_speed: self.label_speed.configure(text=f"Network Speed: {v:.2f} {speed_unit}"))
- self.after(1, lambda p=min(percentage_complete ,99): self.progress_text.configure(text=f"{p:.2f}%"))
- if self.settings_tab.show_fails:
- 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} - Fails: {self.fail_threshold}"))
- else:
- 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)
- else:
- if not self.settings_tab.stopped:
- time_elapsed = time.time() - start_time
- progress = int(current_size / file_size * 100)
- self.after(1, lambda v=progress / 100.0: self.progress_bar.set(v))
-
- current_net_speed = psutil.net_io_counters().bytes_recv
-
- net_speed_bytes = current_net_speed - previous_net_speed
- previous_net_speed = current_net_speed
-
- net_speed, speed_unit = convert_speed(net_speed_bytes)
- elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed)
-
- 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} | Downloading {current_number}/{total_items}"))
- self.after(1, lambda v=net_speed: self.label_speed.configure(text=f"Network Speed: {v:.2f} {speed_unit}"))
- self.after(1, lambda p=progress: self.progress_text.configure(text=f"{p}%"))
- if self.settings_tab.show_fails:
- 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} - Fails: {self.fail_threshold}"))
- else:
- 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)
-
- command = f"+login anonymous app_update 311210 +workshop_download_item 311210 {workshop_id} validate +quit"
- steamcmd_thread = threading.Thread(target=lambda: self.run_steamcmd_command(command, map_folder, workshop_id, queue=True))
- steamcmd_thread.start()
-
- def wait_for_threads():
- update_ui_thread = threading.Thread(target=check_and_update_progress)
- update_ui_thread.daemon = True
- update_ui_thread.start()
- update_ui_thread.join()
-
- self.progress_text.configure(text="0%")
- self.progress_bar.set(0.0)
-
- map_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", workshop_id)
-
- json_file_path = os.path.join(map_folder, "workshop.json")
-
- if os.path.exists(json_file_path):
- self.label_speed.configure(text="Installing...")
- mod_type = extract_json_data(json_file_path, "Type")
- items_file = os.path.join(cwd(), LIBRARY_FILE)
- if self.library_tab.item_exists_in_file(items_file, workshop_id):
- get_folder_name = self.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name")
- if get_folder_name:
- folder_name = get_folder_name
- else:
- try:
- folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
- except:
- folder_name = extract_json_data(json_file_path, "publisherID")
- else:
- try:
- folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
- except:
- folder_name = extract_json_data(json_file_path, "publisherID")
-
- if mod_type == "mod":
- mods_folder = os.path.join(destination_folder, "mods")
- folder_name_path = os.path.join(mods_folder, folder_name, "zone")
- elif mod_type == "map":
- usermaps_folder = os.path.join(destination_folder, "usermaps")
- folder_name_path = os.path.join(usermaps_folder, folder_name, "zone")
- else:
- show_message("Error", f"Invalid workshop type in workshop.json, are you sure this is a map or a mod?., skipping {workshop_id}...", icon="cancel")
- return
-
- os.makedirs(folder_name_path, exist_ok=True)
-
- try:
- self.copy_with_progress(map_folder, folder_name_path)
- except Exception as E:
- show_message("Error", f"Error copying files: {E}", icon="cancel")
-
- if self.settings_tab.clean_on_finish:
- remove_tree(map_folder)
- remove_tree(download_folder)
-
- self.library_tab.update_item(self.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)"))
- self.show_complete_message(message=f"All files were downloaded\nYou can run the game now!\nPS: You have to restart the game \n(pressing launch will launch/restarts)")
- self.label_speed.configure(text="Awaiting Download!")
- elif os.path.exists(json_file_path) and not self.settings_tab.stopped:
- show_message("Error", "Failed to find workshop.json, please try again.", icon="cancel")
- if index == len(items) - 1:
- self.stop_download()
- return
-
- self.button_download.configure(state="disabled")
- self.button_stop.configure(state="normal")
- update_wait_thread = threading.Thread(target=wait_for_threads)
- update_wait_thread.start()
- steamcmd_thread.join()
- update_wait_thread.join()
-
- if index == len(items) - 1:
- 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.after(1, self.label_file_size.configure(text=f"File size: 0KB"))
- self.settings_tab.stopped = True
- self.stop_download()
- return
- finally:
- self.settings_tab.steam_fail_counter = 0
- self.after(1, self.label_file_size.configure(text=f"File size: 0KB"))
- self.stop_download()
- self.is_pressed = False
-
- def download_thread(self, update=None):
- try:
- self.settings_tab.stopped = False
-
- save_config("DestinationFolder" ,self.edit_destination_folder.get())
- save_config("SteamCMDPath" ,self.edit_steamcmd_path.get())
-
- if not check_steamcmd():
- self.show_steam_warning_message()
- return
-
- steamcmd_path = get_steamcmd_path()
-
- if not is_steamcmd_initialized():
- self.show_init_message()
- return
-
- workshop_id = self.edit_workshop_id.get().strip()
-
- destination_folder = self.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.")
- self.stop_download()
- return
-
- if not steamcmd_path or not os.path.exists(steamcmd_path):
- show_message("Error", "Please enter a valid SteamCMD path.")
- self.stop_download()
- return
-
- if not workshop_id.isdigit():
- try:
- if extract_workshop_id(workshop_id).strip().isdigit():
- workshop_id = extract_workshop_id(workshop_id).strip()
- else:
- show_message("Warning", "Please enter a valid Workshop ID/Link.", icon="warning")
- self.stop_download()
- return
- except:
- show_message("Warning", "Please enter a valid Workshop ID/Link.", icon="warning")
- self.stop_download()
- return
-
- ws_file_size = get_workshop_file_size(workshop_id)
- file_size = ws_file_size
-
- if not valid_id(workshop_id):
- show_message("Warning", "Please enter a valid Workshop ID/Link.", 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
-
- if not update:
- if any(workshop_id in item for item in self.library_tab.added_items):
- if self.settings_tab.skip_already_installed:
- show_message("Heads up!, map skipped => Skip is on in settings", f"This item may already be installed, Stopping: {workshop_id}", icon="info")
- self.stop_download()
- return
- show_message("Heads up! map not skipped => Skip is off in settings", f"This item may already be installed: {workshop_id}", icon="info")
-
- 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)
- if not os.path.exists(download_folder):
- os.makedirs(download_folder)
-
- def check_and_update_progress():
- previous_net_speed = 0
- est_downloaded_bytes = 0
- start_time = time.time()
- file_size = ws_file_size
-
- while not self.settings_tab.stopped:
- if self.settings_tab.steamcmd_reset:
- self.settings_tab.steamcmd_reset = False
- previous_net_speed = 0
- 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..."))
- time_elapsed = time.time() - start_time
- elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed)
- if self.settings_tab.show_fails:
- 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} - Fails: {self.fail_threshold}"))
- else:
- 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:
- break
-
- try:
- current_size = sum(os.path.getsize(os.path.join(download_folder, f)) for f in os.listdir(download_folder))
- except:
- try:
- current_size = sum(os.path.getsize(os.path.join(map_folder, f)) for f in os.listdir(map_folder))
- except:
- continue
-
- progress = int(current_size / file_size * 100)
-
- if progress > 100:
- progress = int(current_size / current_size * 100)
- file_size = current_size
- self.after(1, lambda p=progress: self.label_file_size.configure(text=f"Wrong size reported\nActual size: ~{convert_bytes_to_readable(current_size)}"))
-
-
- if self.settings_tab.estimated_progress and not self.settings_tab.stopped:
- time_elapsed = time.time() - start_time
- raw_net_speed = psutil.net_io_counters().bytes_recv
-
- current_net_speed_text = raw_net_speed
- net_speed_bytes = current_net_speed_text - previous_net_speed
- previous_net_speed = current_net_speed_text
-
- current_net_speed = net_speed_bytes
- down_cap = 150000000
- if current_net_speed >= down_cap:
- current_net_speed = 10
-
- est_downloaded_bytes += current_net_speed
-
- percentage_complete = (est_downloaded_bytes / file_size) * 100
-
- progress = min(percentage_complete / 100, 0.99)
-
- net_speed, speed_unit = convert_speed(net_speed_bytes)
-
- elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed)
-
- # print(f"raw_net {raw_net_speed}\ncurrent_net_speed: {current_net_speed}\nest_downloaded_bytes {est_downloaded_bytes}\npercentage_complete {percentage_complete}\nprogress {progress}")
-
- self.after(1, self.progress_bar.set(progress))
- self.after(1, lambda v=net_speed: self.label_speed.configure(text=f"Network Speed: {v:.2f} {speed_unit}"))
- self.after(1, lambda p=min(percentage_complete ,99): self.progress_text.configure(text=f"{p:.2f}%"))
- if self.settings_tab.show_fails:
- 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} - Fails: {self.fail_threshold}"))
- else:
- 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)
- else:
- if not self.settings_tab.stopped:
- time_elapsed = time.time() - start_time
- progress = int(current_size / file_size * 100)
- self.after(1, lambda v=progress / 100.0: self.progress_bar.set(v))
-
- current_net_speed = psutil.net_io_counters().bytes_recv
-
- net_speed_bytes = current_net_speed - previous_net_speed
- previous_net_speed = current_net_speed
-
- net_speed, speed_unit = convert_speed(net_speed_bytes)
- elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed)
-
- self.after(1, lambda v=net_speed: self.label_speed.configure(text=f"Network Speed: {v:.2f} {speed_unit}"))
- self.after(1, lambda p=progress: self.progress_text.configure(text=f"{p}%"))
- if self.settings_tab.show_fails:
- 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} - Fails: {self.fail_threshold}"))
- else:
- 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)
-
- command = f"+login anonymous app_update 311210 +workshop_download_item 311210 {workshop_id} validate +quit"
- steamcmd_thread = threading.Thread(target=lambda: self.run_steamcmd_command(command, map_folder, workshop_id))
- steamcmd_thread.start()
-
- def wait_for_threads():
- update_ui_thread = threading.Thread(target=check_and_update_progress)
- update_ui_thread.daemon = True
- update_ui_thread.start()
- update_ui_thread.join()
-
- self.settings_tab.stopped = True
- self.progress_text.configure(text="0%")
- self.progress_bar.set(0.0)
-
- map_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", workshop_id)
-
- json_file_path = os.path.join(map_folder, "workshop.json")
-
- if os.path.exists(json_file_path):
- self.label_speed.configure(text="Installing...")
- mod_type = extract_json_data(json_file_path, "Type")
- items_file = os.path.join(cwd(), LIBRARY_FILE)
- if self.library_tab.item_exists_in_file(items_file, workshop_id):
- get_folder_name = self.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name")
- if get_folder_name:
- folder_name = get_folder_name
- else:
- try:
- folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
- except:
- folder_name = extract_json_data(json_file_path, "publisherID")
- else:
- try:
- folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
- except:
- folder_name = extract_json_data(json_file_path, "publisherID")
-
- if mod_type == "mod":
- mods_folder = os.path.join(destination_folder, "mods")
- folder_name_path = os.path.join(mods_folder, folder_name, "zone")
- elif mod_type == "map":
- usermaps_folder = os.path.join(destination_folder, "usermaps")
- folder_name_path = os.path.join(usermaps_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")
- self.stop_download()
- return
-
- os.makedirs(folder_name_path, exist_ok=True)
-
- try:
- self.copy_with_progress(map_folder, folder_name_path)
- except Exception as E:
- show_message("Error", f"Error copying files: {E}", icon="cancel")
-
- if self.settings_tab.clean_on_finish:
- 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.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")
- elif os.path.exists(json_file_path) and not self.settings_tab.stopped:
- show_message("Error", "Failed to find workshop.json, please try again.", icon="cancel")
- self.stop_download()
- return
-
- update_wait_thread = threading.Thread(target=wait_for_threads)
- update_wait_thread.start()
- self.button_download.configure(state="disabled")
- self.button_stop.configure(state="normal")
- steamcmd_thread.join()
- update_wait_thread.join()
-
- finally:
- self.settings_tab.steam_fail_counter = 0
- self.stop_download()
- self.is_pressed = False
-
- def copy_with_progress(self, src, dst):
- try:
- total_files = sum([len(files) for root, dirs, files in os.walk(src)])
- progress = 0
-
- def copy_progress(src, dst):
- nonlocal progress
- shutil.copy2(src, dst)
- progress += 1
- self.progress_text.configure(text=f"Copying files: {progress}/{total_files}")
- value = (progress / total_files) * 100
- valuep = value / 100
- self.progress_bar.set(valuep)
-
- try:
- shutil.copytree(src, dst, dirs_exist_ok=True, copy_function=copy_progress)
- except Exception as E:
- show_message("Error", f"Error copying files: {E}", icon="cancel")
- finally:
- self.progress_text.configure(text="0%")
- self.progress_bar.set(0.0)
-
- def stop_download(self, on_close=None):
- self.settings_tab.stopped = True
- self.queue_stop_button = True
- self.settings_tab.steam_fail_counter = 0
- self.is_pressed = False
- self.is_downloading = False
- self.after(1, self.label_file_size.configure(text=f"File size: 0KB"))
-
- if on_close:
- subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- creationflags=subprocess.CREATE_NO_WINDOW)
- return
-
- subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- creationflags=subprocess.CREATE_NO_WINDOW)
-
- self.button_download.configure(state="normal")
- self.button_stop.configure(state="disabled")
- self.progress_text.configure(text="0%")
- self.elapsed_time.configure(text=f"")
- 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()
-
-if __name__ == "__main__":
- app = BOIIIWD()
- app.mainloop()
diff --git a/boiiiwd_old_pyqt.py b/boiiiwd_old_pyqt.py
deleted file mode 100644
index 6fc91b7..0000000
--- a/boiiiwd_old_pyqt.py
+++ /dev/null
@@ -1,999 +0,0 @@
-import os
-import sys
-import re
-import subprocess
-import configparser
-import json
-import shutil
-import zipfile
-import psutil
-import requests
-import time
-import threading
-from bs4 import BeautifulSoup
-from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QDialog, \
- QVBoxLayout, QMessageBox, QHBoxLayout, QProgressBar, QSizePolicy, QFileDialog, QCheckBox, QSpacerItem
-from PyQt5.QtCore import Qt, QThread, pyqtSignal
-from PyQt5.QtCore import QCoreApplication, QSettings
-from PyQt5.QtGui import QIcon, QPixmap, QCloseEvent
-import webbrowser
-import qdarktheme
-
-VERSION = "v0.1.3"
-GITHUB_REPO = "faroukbmiled/BOIIIWD"
-LATEST_RELEASE_URL = "https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip"
-UPDATER_FOLDER = "update"
-CONFIG_FILE_PATH = "config.ini"
-global stopped, steampid, console, up_cancelled
-steampid = None
-stopped = False
-console = False
-up_cancelled = False
-
-def get_latest_release_version():
- try:
- 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()
- return data["tag_name"]
- except requests.exceptions.RequestException as e:
- 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
- echo Terminating BOIIIWD.exe...
- taskkill /im "{program_name}" /t /f
-
- echo Replacing BOIIIWD.exe...
- cd "{updater_folder}"
- taskkill /im "{program_name}" /t /f
- move /y "{new_exe}" "../"{program_name}""
-
- echo Starting BOIIIWD.exe...
- cd ..
- start "" "{current_exe}"
-
- echo Exiting!
- exit
- """
-
- script_path = os.path.join(updater_folder, "boiiiwd_updater.bat")
- with open(script_path, "w") as script_file:
- script_file.write(script_content)
-
- return script_path
-
-def cwd():
- if getattr(sys, 'frozen', False):
- return os.path.dirname(sys.executable)
- else:
- return os.path.dirname(os.path.abspath(__file__))
-
-def extract_workshop_id(link):
- try:
- pattern = r'(?<=id=)(\d+)'
- match = re.search(pattern, link)
-
- if match:
- return match.group(0)
- else:
- return None
- except:
- return None
-
-def check_steamcmd():
- steamcmd_path = get_steamcmd_path()
- steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe")
-
- if not os.path.exists(steamcmd_exe_path):
- return False
-
- return True
-
-def initialize_steam():
- 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.wait()
-
- show_message("Done!", "BOIIIWD is ready for action.", icon=QMessageBox.Information)
- except:
- show_message("Done!", "An error occurred please check your paths and try again.")
-
-def valid_id(workshop_id):
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
- response = requests.get(url)
- response.raise_for_status()
- content = response.text
- soup = BeautifulSoup(content, "html.parser")
-
- try:
- soup.find("div", class_="rightDetailsBlock").text.strip()
- soup.find("div", class_="workshopItemTitle").text.strip()
- soup.find("div", class_="detailsStatRight").text.strip()
- stars_div = soup.find("div", class_="fileRatingDetails")
- stars_div.find("img")["src"]
- return True
- except:
- return False
-
-def convert_speed(speed_bytes):
- if speed_bytes < 1024:
- return speed_bytes, "B/s"
- elif speed_bytes < 1024 * 1024:
- return speed_bytes / 1024, "KB/s"
- elif speed_bytes < 1024 * 1024 * 1024:
- return speed_bytes / (1024 * 1024), "MB/s"
- else:
- return speed_bytes / (1024 * 1024 * 1024), "GB/s"
-
-def create_default_config():
- config = configparser.ConfigParser()
- config["Settings"] = {
- "SteamCMDPath": cwd(),
- "DestinationFolder": "",
- "checkforupdtes": "on",
- "console": "off"
- }
- with open(CONFIG_FILE_PATH, "w") as config_file:
- config.write(config_file)
-
-def run_steamcmd_command(command):
- steamcmd_path = get_steamcmd_path()
- show_console = subprocess.CREATE_NO_WINDOW
- if console:
- show_console = subprocess.CREATE_NEW_CONSOLE
-
- process = subprocess.Popen(
- [steamcmd_path + "\steamcmd.exe"] + command.split(),
- stdout=None if console else subprocess.PIPE,
- stderr=None if console else subprocess.PIPE,
- text=True,
- bufsize=1,
- universal_newlines=True,
- creationflags=show_console
- )
-
- global steampid
- steampid = process.pid
-
- if process.poll() is not None:
- return process.returncode
-
- process.communicate()
-
- if process.returncode != 0:
- show_message("Warning", "SteamCMD encountered an error while downloading, try again!")
-
- return process.returncode
-
-def get_steamcmd_path():
- config = configparser.ConfigParser()
- config.read(CONFIG_FILE_PATH)
- return config.get("Settings", "SteamCMDPath", fallback=cwd())
-
-def config_check_for_updates(state=None):
- if state:
- config = configparser.ConfigParser()
- config.read(CONFIG_FILE_PATH)
- config["Settings"]["checkforupdtes"] = state
- with open(CONFIG_FILE_PATH, "w") as config_file:
- config.write(config_file)
- return
- config = configparser.ConfigParser()
- config.read(CONFIG_FILE_PATH)
- return config.get("Settings", "checkforupdtes", fallback="on")
-
-def config_console_state(state=None):
- if state:
- config = configparser.ConfigParser()
- config.read(CONFIG_FILE_PATH)
- config["Settings"]["console"] = state
- with open(CONFIG_FILE_PATH, "w") as config_file:
- config.write(config_file)
- return
- config = configparser.ConfigParser()
- config.read(CONFIG_FILE_PATH)
- return config.get("Settings", "console", fallback="off")
-
-def extract_json_data(json_path):
- with open(json_path, "r") as json_file:
- data = json.load(json_file)
- return data["Type"], data["FolderName"]
-
-def convert_bytes_to_readable(size_in_bytes):
- for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
- if size_in_bytes < 1024.0:
- 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)
- soup = BeautifulSoup(response.text, "html.parser")
- file_size_element = soup.find("div", class_="detailsStatRight")
-
- try:
- 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)
- 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)
- return file_size_in_bytes
- return None
- except:
- return None
-
-def update_progress_bar(current_size, file_size, progress_bar):
- if file_size is not None:
- progress = int(current_size / file_size * 100)
- progress_bar.setValue(progress)
-
-def check_and_update_progress(file_size, folder_name_path, progress_bar, speed_label):
- previous_net_speed = 0
-
- while not stopped:
- current_size = sum(os.path.getsize(os.path.join(folder_name_path, f)) for f in os.listdir(folder_name_path))
- update_progress_bar(current_size, file_size, progress_bar)
-
- current_net_speed = psutil.net_io_counters().bytes_recv
-
- net_speed_bytes = current_net_speed - previous_net_speed
- previous_net_speed = current_net_speed
-
- net_speed, speed_unit = convert_speed(net_speed_bytes)
-
- speed_label.setText(f"Network Speed: {net_speed:.2f} {speed_unit}")
-
- QCoreApplication.processEvents()
- time.sleep(1)
-
-def download_workshop_map(workshop_id, destination_folder, progress_bar, speed_label):
- file_size = get_workshop_file_size(workshop_id)
- if file_size is None:
- show_message("Error", "Failed to retrieve file size.")
- return
-
- download_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "downloads", "311210", workshop_id)
- if not os.path.exists(download_folder):
- os.makedirs(download_folder)
-
- command = f"+login anonymous +workshop_download_item 311210 {workshop_id} +quit"
- progress_thread = threading.Thread(target=check_and_update_progress, args=(file_size, download_folder, progress_bar, speed_label))
- progress_thread.daemon = True
- progress_thread.start()
-
- run_steamcmd_command(command)
-
- global stopped
- stopped = True
- progress_bar.setValue(100)
-
- map_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", workshop_id)
-
- json_file_path = os.path.join(map_folder, "workshop.json")
-
- if os.path.exists(json_file_path):
- global mod_type
- mod_type, folder_name = extract_json_data(json_file_path)
-
- if mod_type == "mod":
- mods_folder = os.path.join(destination_folder, "mods")
- folder_name_path = os.path.join(mods_folder, folder_name, "zone")
- elif mod_type == "map":
- usermaps_folder = os.path.join(destination_folder, "usermaps")
- folder_name_path = os.path.join(usermaps_folder, folder_name, "zone")
- else:
- show_message("Error", "Invalid map type in workshop.json.")
- return
-
- os.makedirs(folder_name_path, exist_ok=True)
-
- try:
- shutil.copytree(map_folder, folder_name_path, dirs_exist_ok=True)
- except Exception as E:
- show_message("Error", f"Error copying files: {E}")
-
- show_message("Download Complete", f"{mod_type} files are downloaded at \n{folder_name_path}\nYou can run the game now!", icon=QMessageBox.Information)
-
-def show_message(title, message, icon=QMessageBox.Warning, exit_on_close=False):
- msg = QMessageBox()
- msg.setWindowTitle(title)
- msg.setWindowIcon(QIcon('ryuk.ico'))
- msg.setText(message)
- msg.setIcon(icon)
-
- if exit_on_close:
- msg.setStandardButtons(QMessageBox.Ok | QMessageBox.No)
- msg.setDefaultButton(QMessageBox.Ok)
- result = msg.exec_()
-
- if result == QMessageBox.No:
- sys.exit(0)
- else:
- msg.exec_()
-
-class UpdatePorgressThread(QThread):
- global up_cancelled
- progress_update = pyqtSignal(int)
-
- def __init__(self, label_progress, progress_bar, label_size):
- super().__init__()
- self.label_progress = label_progress
- self.progress_bar = progress_bar
- self.label_size = label_size
- self.cancelled = False
-
- def run(self):
- try:
- update_dir = os.path.join(os.getcwd(), UPDATER_FOLDER)
- response = requests.get(LATEST_RELEASE_URL, stream=True)
- response.raise_for_status()
- current_exe = sys.argv[0]
- program_name = os.path.basename(current_exe)
- new_exe = os.path.join(update_dir, "BOIIIWD.exe")
-
- if not os.path.exists(update_dir):
- os.makedirs(update_dir)
-
- zip_path = os.path.join(update_dir, "latest_version.zip")
- total_size = int(response.headers.get('content-length', 0))
- size = convert_bytes_to_readable(total_size)
- self.label_size.setText(f"Size: {size}")
-
- with open(zip_path, "wb") as zip_file:
- chunk_size = 8192
- current_size = 0
-
- for chunk in response.iter_content(chunk_size=chunk_size):
- if up_cancelled:
- break
-
- if chunk:
- zip_file.write(chunk)
- current_size += len(chunk)
- progress = int(current_size / total_size * 100)
- self.progress_update.emit(progress)
- QCoreApplication.processEvents()
-
- if not up_cancelled:
- with zipfile.ZipFile(zip_path, "r") as zip_ref:
- zip_ref.extractall(update_dir)
-
- self.label_progress.setText("Update installed successfully!")
- time.sleep(1)
- script_path = create_update_script(current_exe, new_exe, update_dir, program_name)
- subprocess.run(('cmd', '/C', 'start', '', fr'{script_path}'))
- sys.exit(0)
- else:
- if os.path.exists(zip_path):
- os.remove(fr"{zip_path}")
- self.label_progress.setText("Update cancelled.")
-
- except Exception as e:
- self.label_progress.setText("Error installing the update.")
- show_message("Warning", f"Error installing the update: {e}")
-
-class UpdateProgressWindow(QDialog):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("Updating...")
- self.setWindowIcon(QIcon('ryuk.ico'))
-
- layout = QVBoxLayout()
-
- info_layout = QHBoxLayout()
- self.label_progress = QLabel("Downloading latest update from Github...")
- info_layout.addWidget(self.label_progress, 3)
-
- self.label_size = QLabel("File size: 0KB")
- info_layout.addWidget(self.label_size, 1)
-
- layout.addLayout(info_layout)
-
- self.progress_bar = QProgressBar()
- layout.addWidget(self.progress_bar)
-
- spacer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
- layout.addSpacerItem(spacer)
-
- button_layout = QHBoxLayout()
- self.cancel_button = QPushButton("Cancel")
- self.cancel_button.clicked.connect(self.cancel_update)
- button_layout.addItem(QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
- button_layout.addWidget(self.cancel_button)
- layout.addLayout(button_layout)
-
- self.setLayout(layout)
-
- global up_cancelled
- self.thread = None
- up_cancelled = False
-
-
- def update_progress(self, value):
- self.progress_bar.setValue(value)
-
- def start_update(self):
- self.thread = UpdatePorgressThread(self.label_progress, self.progress_bar, self.label_size)
- self.thread.progress_update.connect(self.update_progress)
- self.thread.finished.connect(self.on_update_finished)
- self.thread.start()
-
- def on_update_finished(self):
- """code"""
- # self.accept()
-
- def cancel_update(self):
- global up_cancelled
- up_cancelled = True
- self.label_progress.setText("Update cancelled.")
-
- def closeEvent(self, event: QCloseEvent):
- global up_cancelled
- if not up_cancelled:
- self.cancel_update()
- super().closeEvent(event)
-
-class DownloadThread(QThread):
- finished = pyqtSignal()
-
- def __init__(self, workshop_id, destination_folder, progress_bar, label_speed):
- super().__init__()
- self.workshop_id = workshop_id
- self.destination_folder = destination_folder
- self.progress_bar = progress_bar
- self.label_speed = label_speed
-
- def run(self):
- download_workshop_map(self.workshop_id, self.destination_folder, self.progress_bar, self.label_speed)
- self.finished.emit()
-
-class WorkshopDownloaderApp(QWidget):
- def __init__(self):
- super().__init__()
- self.initUI()
-
- if not check_steamcmd():
- self.show_warning_message()
-
- self.download_thread = None
- self.button_download.setEnabled(True)
- self.button_stop.setEnabled(False)
-
- def show_warning_message(self):
- msg_box = QMessageBox(self)
- msg_box.setWindowTitle("Warning")
- msg_box.setWindowIcon(QIcon('ryuk.ico'))
- msg_box.setText("steamcmd.exe was not found in the specified directory.\nPress Download to get it or Press OK and select it from there!.")
- msg_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
-
- download_button = msg_box.addButton("Download", QMessageBox.AcceptRole)
- download_button.clicked.connect(self.download_steamcmd)
- msg_box.setDefaultButton(download_button)
-
- result = msg_box.exec_()
- if result == QMessageBox.Cancel:
- sys.exit(0)
-
- def download_steamcmd(self):
- self.edit_steamcmd_path.setText(cwd())
- self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
- steamcmd_url = "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"
- steamcmd_zip_path = os.path.join(cwd(), "steamcmd.zip")
-
- try:
- response = requests.get(steamcmd_url)
- response.raise_for_status()
-
- with open(steamcmd_zip_path, "wb") as zip_file:
- zip_file.write(response.content)
-
- with zipfile.ZipFile(steamcmd_zip_path, "r") as zip_ref:
- zip_ref.extractall(cwd())
-
- if check_steamcmd():
- os.remove(fr"{steamcmd_zip_path}")
- show_message("Success", "SteamCMD has been downloaded ,Press ok to initialize it.", icon=QMessageBox.Information, exit_on_close=True)
- initialize_steam()
-
- else:
- show_message("Error", "Failed to find steamcmd.exe after extraction.\nMake you sure to select the correct SteamCMD path (which is the current BOIIIWD path)")
- os.remove(fr"{steamcmd_zip_path}")
- except requests.exceptions.RequestException as e:
- show_message("Error", f"Failed to download SteamCMD: {e}")
- os.remove(fr"{steamcmd_zip_path}")
- except zipfile.BadZipFile:
- show_message("Error", "Failed to extract SteamCMD. The downloaded file might be corrupted.")
- os.remove(fr"{steamcmd_zip_path}")
-
-
- def check_for_updates(self, ignore_up_todate=False):
- try:
- latest_version = get_latest_release_version()
- current_version = VERSION
-
- if latest_version and latest_version != current_version:
- msg_box = QMessageBox()
- msg_box.setWindowTitle("Update Available")
- msg_box.setWindowIcon(QIcon('ryuk.ico'))
- msg_box.setText(f"An update is available!, Do you want to install it?\n\nCurrent Version: {current_version}\nLatest Version: {latest_version}")
- msg_box.setIcon(QMessageBox.Information)
- msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Open)
- msg_box.setDefaultButton(QMessageBox.Yes)
- result = msg_box.exec_()
-
- if result == QMessageBox.Open:
- webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest")
-
- if result == QMessageBox.Yes:
- update_progress_window = UpdateProgressWindow()
- update_progress_window.start_update()
- update_progress_window.exec_()
- elif latest_version == current_version:
- if ignore_up_todate:
- return
- msg_box = QMessageBox()
- msg_box.setWindowTitle("Up to Date!")
- msg_box.setWindowIcon(QIcon('ryuk.ico'))
- msg_box.setText(f"No Updates Available!")
- msg_box.setIcon(QMessageBox.Information)
- msg_box.setStandardButtons(QMessageBox.Ok)
- msg_box.setDefaultButton(QMessageBox.Ok)
- result = msg_box.exec_()
- except Exception as e:
- show_message("Error", f"Error while checking for updates: \n{e}")
-
- def initUI(self):
- self.setWindowTitle(f'BOIII Workshop Downloader {VERSION}-beta')
- self.setWindowIcon(QIcon('ryuk.ico'))
- self.setGeometry(100, 100, 400, 200)
- self.settings = QSettings("MyApp", "MyWindow")
- self.restore_geometry()
-
- layout = QVBoxLayout()
-
- browse_layout = QHBoxLayout()
-
- self.label_workshop_id = QLabel("Enter the Workshop ID or Link of the map/mod you want to download:")
- browse_layout.addWidget(self.label_workshop_id, 3)
-
- self.button_browse = QPushButton("Browse")
- self.button_browse.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- self.button_browse.clicked.connect(self.open_browser)
- browse_layout.addWidget(self.button_browse, 1)
-
- layout.addLayout(browse_layout)
-
- info_workshop_layout = QHBoxLayout()
-
- self.edit_workshop_id = QLineEdit()
- self.edit_workshop_id.setPlaceholderText("Workshop ID/Link => Press info to see map/mod info")
- self.edit_workshop_id.textChanged.connect(self.reset_file_size)
- info_workshop_layout.addWidget(self.edit_workshop_id, 3)
-
- layout.addLayout(info_workshop_layout)
- self.info_button = QPushButton("Info")
- self.info_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- self.info_button.clicked.connect(self.show_map_info)
- info_workshop_layout.addWidget(self.info_button, 1)
-
- self.label_destination_folder = QLabel("Enter Your BOIII folder:")
- layout.addWidget(self.label_destination_folder, 3)
-
- Boiii_Input = QHBoxLayout()
- self.edit_destination_folder = QLineEdit()
- self.edit_destination_folder.setPlaceholderText("Your BOIII Instalation folder")
- Boiii_Input.addWidget(self.edit_destination_folder, 90)
-
- layout.addLayout(Boiii_Input)
-
- self.button_BOIII_browse = QPushButton("Select")
- self.button_BOIII_browse.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- self.button_BOIII_browse.clicked.connect(self.open_BOIII_browser)
- Boiii_Input.addWidget(self.button_BOIII_browse, 10)
-
- self.label_steamcmd_path = QLabel("Enter SteamCMD path (default):")
- layout.addWidget(self.label_steamcmd_path)
-
- steamcmd_path = QHBoxLayout()
- self.edit_steamcmd_path = QLineEdit()
- steamcmd_path.addWidget(self.edit_steamcmd_path, 90)
-
- self.button_steamcmd_browse = QPushButton("Select")
- self.button_steamcmd_browse.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- self.button_steamcmd_browse.clicked.connect(self.open_steamcmd_path_browser)
- steamcmd_path.addWidget(self.button_steamcmd_browse, 10)
-
- layout.addLayout(steamcmd_path)
- layout.addSpacing(10)
-
- buttons_layout = QHBoxLayout()
-
- self.button_download = QPushButton("Download")
- self.button_download.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- self.button_download.clicked.connect(self.download_map)
- buttons_layout.addWidget(self.button_download, 70)
-
- self.button_stop = QPushButton("Stop")
- self.button_stop.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- self.button_stop.clicked.connect(self.stop_download)
- buttons_layout.addWidget(self.button_stop, 25)
-
- layout.addLayout(buttons_layout)
-
- InfoBar = QHBoxLayout()
-
- self.label_speed = QLabel("Network Speed: 0 KB/s")
- InfoBar.addWidget(self.label_speed, 3)
-
- self.label_file_size = QLabel("File size: 0KB")
- InfoBar.addWidget(self.label_file_size, 1)
-
- InfoWidget = QWidget()
- InfoWidget.setLayout(InfoBar)
-
- layout.addWidget(InfoWidget)
-
- self.progress_bar = QProgressBar()
- layout.addWidget(self.progress_bar, 75)
-
- spacer = QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)
- layout.addSpacerItem(spacer)
-
- check_for_update_layout = QHBoxLayout()
- check_update_button = QPushButton("Check for Updates")
- check_update_button.clicked.connect(self.check_for_updates)
- check_update_button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
-
- self.check_for_update_layout = QVBoxLayout()
- self.check_for_update_layout.addWidget(check_update_button)
-
- self.show_more_button = QPushButton("Launch boiii")
- self.show_more_button.clicked.connect(self.launch_boiii)
-
- check_for_update_layout = QHBoxLayout()
- check_for_update_layout.addWidget(check_update_button)
-
- self.check_for_updates_checkbox = QPushButton("Settings")
- self.check_for_updates_checkbox.clicked.connect(self.open_settings_dialog)
-
-
- check_for_update_layout = QHBoxLayout()
- check_for_update_layout.addWidget(check_update_button)
- check_for_update_layout.addWidget(self.check_for_updates_checkbox)
- check_for_update_layout.addWidget(self.show_more_button)
-
- layout.addLayout(check_for_update_layout)
-
- self.setLayout(layout)
-
- self.load_config()
-
- if config_check_for_updates() == "on":
- self.check_for_updates(ignore_up_todate=True)
-
- try:
- global console
- if config_console_state() == "on":
- console = True
- return 1
- else:
- console = False
- return 0
- except:
- pass
-
- def download_map(self):
- global stopped
- stopped = False
- self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
-
- if not check_steamcmd():
- self.show_warning_message()
- return
-
- steamcmd_path = get_steamcmd_path()
- steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe")
- steamcmd_size = os.path.getsize(steamcmd_exe_path)
- if steamcmd_size < 3 * 1024 * 1024:
- show_message("Warning", "SteamCMD is not initialized, Press OK to do so!\nProgram may go unresponsive until SteamCMD is finished downloading.", icon=QMessageBox.Warning, exit_on_close=True)
- initialize_steam()
- return
-
- workshop_id = self.edit_workshop_id.text().strip()
- if not workshop_id.isdigit():
- try:
- if extract_workshop_id(workshop_id).strip().isdigit():
- workshop_id = extract_workshop_id(workshop_id).strip()
- else:
- QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
- return
- except:
- QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
- return
-
- if not valid_id(workshop_id):
- QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
- return
-
- destination_folder = self.edit_destination_folder.text()
- steamcmd_path = self.edit_steamcmd_path.text()
- self.label_file_size.setText(f"File size: {get_workshop_file_size(workshop_id, raw=True)}")
-
- if not destination_folder:
- show_message("Error", "Please select a destination folder.")
- return
-
- if not steamcmd_path:
- show_message("Error", "Please enter the SteamCMD path.")
- return
-
- self.button_stop.setEnabled(True)
- self.progress_bar.setValue(0)
- self.button_download.setEnabled(False)
-
- self.download_thread = DownloadThread(workshop_id, destination_folder, self.progress_bar, self.label_speed)
- self.download_thread.finished.connect(self.on_download_finished)
- self.download_thread.start()
-
- def stop_download(self):
- global stopped
- stopped = True
-
- subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-
- if self.download_thread and self.download_thread.isRunning():
- self.download_thread.terminate()
-
- self.button_download.setEnabled(True)
- self.button_stop.setEnabled(False)
- self.progress_bar.setValue(0)
- self.label_speed.setText(f"Network Speed: {0:.2f} KB/s")
- self.label_file_size.setText(f"File size: 0KB")
-
- def open_BOIII_browser(self):
- selected_folder = QFileDialog.getExistingDirectory(self, "Select BOIII Folder", "")
- if selected_folder:
- self.edit_destination_folder.setText(selected_folder)
- self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
-
- def open_steamcmd_path_browser(self):
- selected_folder = QFileDialog.getExistingDirectory(self, "Select SteamCMD Folder", "")
- if selected_folder:
- self.edit_steamcmd_path.setText(selected_folder)
- self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
-
- def on_download_finished(self):
- self.button_download.setEnabled(True)
- self.progress_bar.setValue(0)
- self.label_speed.setText(f"Network Speed: {0:.2f} KB/s")
- self.label_file_size.setText(f"File size: 0KB")
- self.button_stop.setEnabled(False)
- self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
-
- def open_browser(self):
- link = "https://steamcommunity.com/app/311210/workshop/"
- webbrowser.open(link)
-
- def load_config(self):
- config = configparser.ConfigParser()
- if os.path.exists(CONFIG_FILE_PATH):
- config.read(CONFIG_FILE_PATH)
- destination_folder = config.get("Settings", "DestinationFolder", fallback="")
- steamcmd_path = config.get("Settings", "SteamCMDPath", fallback=cwd())
- self.edit_destination_folder.setText(destination_folder)
- self.edit_steamcmd_path.setText(steamcmd_path)
- else:
- create_default_config()
-
- def save_config(self, destination_folder, steamcmd_path):
- config = configparser.ConfigParser()
- config.read(CONFIG_FILE_PATH)
- config.set("Settings", "DestinationFolder", destination_folder)
- config.set("Settings", "SteamCMDPath", steamcmd_path)
- with open(CONFIG_FILE_PATH, "w") as config_file:
- config.write(config_file)
-
- def reset_file_size(self):
- self.label_file_size.setText(f"File size: 0KB")
-
- def show_map_info(self):
- workshop_id = self.edit_workshop_id.text().strip()
-
- if not workshop_id:
- QMessageBox.warning(self, "Warning", "Please enter a Workshop ID first.")
- return
-
- if not workshop_id.isdigit():
- try:
- if extract_workshop_id(workshop_id).strip().isdigit():
- workshop_id = extract_workshop_id(workshop_id).strip()
- else:
- QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
- return
- except:
- QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
- return
-
- self.label_file_size.setText(f"File size: {get_workshop_file_size(workshop_id, raw=True)}")
- try:
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
- response = requests.get(url)
- response.raise_for_status()
- content = response.text
-
- soup = BeautifulSoup(content, "html.parser")
-
- try:
- map_mod_type = soup.find("div", class_="rightDetailsBlock").text.strip()
- map_name = soup.find("div", class_="workshopItemTitle").text.strip()
- map_size = soup.find("div", class_="detailsStatRight").text.strip()
- stars_div = soup.find("div", class_="fileRatingDetails")
- stars = stars_div.find("img")["src"]
- except:
- QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
- return
-
- try:
- preview_image_element = soup.find("img", id="previewImage")
- workshop_item_image_url = preview_image_element["src"]
- except:
- preview_image_element = soup.find("img", id="previewImageMain")
- workshop_item_image_url = preview_image_element["src"]
-
- image_response = requests.get(workshop_item_image_url)
- image_response.raise_for_status()
-
- stars_response = requests.get(stars)
- stars_response.raise_for_status()
-
- pixmap = QPixmap()
- pixmap.loadFromData(image_response.content)
-
- pixmap_stars = QPixmap()
- pixmap_stars.loadFromData(stars_response.content)
-
- label = QLabel(self)
- label.setPixmap(pixmap)
- label.setAlignment(Qt.AlignCenter)
-
- label_stars = QLabel(self)
- label_stars.setPixmap(pixmap_stars)
- label_stars.setAlignment(Qt.AlignCenter)
-
- info = (
- f"Name: {map_name}
"
- f"Type: {map_mod_type}
"
- f"Size: {map_size}
"
- )
-
- msg_box = QMessageBox(self)
- msg_box.setWindowTitle("Map/Mod Information")
- msg_box.setWindowIcon(QIcon('ryuk.ico'))
- msg_box.setIconPixmap(pixmap)
- msg_box.setInformativeText(info)
- msg_box.setDetailedText(f"Stars: {stars}\nLink: {url}")
- msg_box.setStandardButtons(QMessageBox.Ok)
-
- msg_box.exec_()
-
- except requests.exceptions.RequestException as e:
- show_message("Error", f"Failed to fetch map information.\nError: {e}")
-
- def launch_boiii(self):
- try:
- boiii_path = os.path.join(self.edit_destination_folder.text(), "boiii.exe")
- subprocess.Popen([boiii_path], cwd=self.edit_destination_folder.text())
- except Exception as e:
- show_message("Error: Failed to launch BOIII", f"Failed to launch boiii.exe\nMake sure to put in your correct boiii path\n{e}")
-
- def open_settings_dialog(self):
- settings_dialog = SettingsDialog()
- settings_dialog.exec_()
-
- def closeEvent(self, event):
- self.settings.setValue("geometry", self.saveGeometry())
- super().closeEvent(event)
-
- def restore_geometry(self):
- geometry = self.settings.value("geometry", None)
- if geometry is not None:
- self.restoreGeometry(geometry)
-
-class SettingsDialog(QDialog):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("Settings")
- self.setWindowIcon(QIcon('ryuk.ico'))
- self.setGeometry(50, 50, 250, 120)
- self.settings = QSettings("MyApp2", "MyWindow2")
- self.restore_geometry()
- self.initUI()
-
- def initUI(self):
- layout = QVBoxLayout()
-
- self.check_updates_checkbox = QCheckBox("Check for updates on launch")
- self.check_updates_checkbox.setChecked(self.load_settings(updates=True))
- layout.addWidget(self.check_updates_checkbox)
-
- buttons_layout = QHBoxLayout()
- self.checkbox_show_console = QCheckBox("Console (On Download)", self)
- self.checkbox_show_console.setChecked(self.load_settings(console=True))
- tooltip_text = "Toggle SteamCMD console\nPlease don't close the Console If you want to stop press the Stop boutton."
- self.checkbox_show_console.setToolTip(tooltip_text)
-
- buttons_layout.addWidget(self.checkbox_show_console, 5)
-
- layout.addLayout(buttons_layout)
-
- save_button = QPushButton("Save")
- save_button.setFixedWidth(60)
- save_button.clicked.connect(self.save_settings)
- layout.addWidget(save_button, alignment=Qt.AlignLeft)
-
- self.setLayout(layout)
-
- def save_settings(self):
- global console
- if self.check_updates_checkbox.isChecked():
- config_check_for_updates(state="on")
- else:
- config_check_for_updates(state="off")
-
- if self.checkbox_show_console.isChecked():
- config_console_state(state="on")
- console = True
- else:
- config_console_state(state="off")
- console = False
-
- self.accept()
-
- def load_settings(self, console=None, updates=None):
- if updates:
- if config_check_for_updates() == "on":
- return 1
- else:
- return 0
- if console:
- if config_console_state() == "on":
- console = True
- return 1
- else:
- console = False
- return 0
-
- def closeEvent(self, event):
- self.settings.setValue("geometry", self.saveGeometry())
- super().closeEvent(event)
-
- def restore_geometry(self):
- geometry = self.settings.value("geometry", None)
- if geometry is not None:
- self.restoreGeometry(geometry)
-
-if __name__ == "__main__":
- app = QApplication(sys.argv)
- qdarktheme.setup_theme()
-
- if not os.path.exists(CONFIG_FILE_PATH):
- create_default_config()
-
- window = WorkshopDownloaderApp()
- window.show()
-
- sys.exit(app.exec_())
diff --git a/boiiiwd_package/__init__.py b/boiiiwd_package/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/boiiiwd_package/boiiiwd.py b/boiiiwd_package/boiiiwd.py
index dc9c8a9..0095ea5 100644
--- a/boiiiwd_package/boiiiwd.py
+++ b/boiiiwd_package/boiiiwd.py
@@ -1,5 +1,4 @@
-from src import main
+import src.shared_vars as main_app
if __name__ == "__main__":
- app = main.BOIIIWD()
- app.mainloop()
+ main_app.app.mainloop()
diff --git a/boiiiwd_package/src/resources/Refresh_icon.svg.png b/boiiiwd_package/resources/Refresh_icon.svg.png
similarity index 100%
rename from boiiiwd_package/src/resources/Refresh_icon.svg.png
rename to boiiiwd_package/resources/Refresh_icon.svg.png
diff --git a/boiiiwd_package/resources/b_map_image.png b/boiiiwd_package/resources/b_map_image.png
new file mode 100644
index 0000000..61f9bb5
Binary files /dev/null and b/boiiiwd_package/resources/b_map_image.png differ
diff --git a/boiiiwd_package/resources/b_mod_image.png b/boiiiwd_package/resources/b_mod_image.png
new file mode 100644
index 0000000..9232821
Binary files /dev/null and b/boiiiwd_package/resources/b_mod_image.png differ
diff --git a/boiiiwd_package/src/resources/boiiiwd_blue.json b/boiiiwd_package/resources/boiiiwd_blue.json
similarity index 100%
rename from boiiiwd_package/src/resources/boiiiwd_blue.json
rename to boiiiwd_package/resources/boiiiwd_blue.json
diff --git a/boiiiwd_package/src/resources/boiiiwd_ghost.json b/boiiiwd_package/resources/boiiiwd_ghost.json
similarity index 100%
rename from boiiiwd_package/src/resources/boiiiwd_ghost.json
rename to boiiiwd_package/resources/boiiiwd_ghost.json
diff --git a/boiiiwd_package/src/resources/boiiiwd_grey.json b/boiiiwd_package/resources/boiiiwd_grey.json
similarity index 100%
rename from boiiiwd_package/src/resources/boiiiwd_grey.json
rename to boiiiwd_package/resources/boiiiwd_grey.json
diff --git a/boiiiwd_package/src/resources/boiiiwd_neonbanana.json b/boiiiwd_package/resources/boiiiwd_neonbanana.json
similarity index 100%
rename from boiiiwd_package/src/resources/boiiiwd_neonbanana.json
rename to boiiiwd_package/resources/boiiiwd_neonbanana.json
diff --git a/boiiiwd_package/src/resources/boiiiwd_obsidian.json b/boiiiwd_package/resources/boiiiwd_obsidian.json
similarity index 100%
rename from boiiiwd_package/src/resources/boiiiwd_obsidian.json
rename to boiiiwd_package/resources/boiiiwd_obsidian.json
diff --git a/boiiiwd_package/src/resources/boiiiwd_test.json b/boiiiwd_package/resources/boiiiwd_test.json
similarity index 100%
rename from boiiiwd_package/src/resources/boiiiwd_test.json
rename to boiiiwd_package/resources/boiiiwd_test.json
diff --git a/boiiiwd_package/src/resources/boiiiwd_theme.json b/boiiiwd_package/resources/boiiiwd_theme.json
similarity index 100%
rename from boiiiwd_package/src/resources/boiiiwd_theme.json
rename to boiiiwd_package/resources/boiiiwd_theme.json
diff --git a/boiiiwd_package/src/resources/map_image.png b/boiiiwd_package/resources/map_image.png
similarity index 100%
rename from boiiiwd_package/src/resources/map_image.png
rename to boiiiwd_package/resources/map_image.png
diff --git a/boiiiwd_package/src/resources/mod_image.png b/boiiiwd_package/resources/mod_image.png
similarity index 100%
rename from boiiiwd_package/src/resources/mod_image.png
rename to boiiiwd_package/resources/mod_image.png
diff --git a/boiiiwd_package/ryuk.ico b/boiiiwd_package/resources/ryuk.ico
similarity index 100%
rename from boiiiwd_package/ryuk.ico
rename to boiiiwd_package/resources/ryuk.ico
diff --git a/boiiiwd_package/src/resources/ryuk.png b/boiiiwd_package/resources/ryuk.png
similarity index 100%
rename from boiiiwd_package/src/resources/ryuk.png
rename to boiiiwd_package/resources/ryuk.png
diff --git a/boiiiwd_package/src/resources/sett10.png b/boiiiwd_package/resources/sett10.png
similarity index 100%
rename from boiiiwd_package/src/resources/sett10.png
rename to boiiiwd_package/resources/sett10.png
diff --git a/boiiiwd_package/src/resources/update_icon.png b/boiiiwd_package/resources/update_icon.png
similarity index 100%
rename from boiiiwd_package/src/resources/update_icon.png
rename to boiiiwd_package/resources/update_icon.png
diff --git a/CTkListbox/__init__.py b/boiiiwd_package/src/CTkListbox/__init__.py
similarity index 100%
rename from CTkListbox/__init__.py
rename to boiiiwd_package/src/CTkListbox/__init__.py
diff --git a/CTkListbox/ctk_listbox.py b/boiiiwd_package/src/CTkListbox/ctk_listbox.py
similarity index 100%
rename from CTkListbox/ctk_listbox.py
rename to boiiiwd_package/src/CTkListbox/ctk_listbox.py
diff --git a/CTkToolTip/__init__.py b/boiiiwd_package/src/CTkToolTip/__init__.py
similarity index 99%
rename from CTkToolTip/__init__.py
rename to boiiiwd_package/src/CTkToolTip/__init__.py
index 5adf78b..28d3642 100644
--- a/CTkToolTip/__init__.py
+++ b/boiiiwd_package/src/CTkToolTip/__init__.py
@@ -8,4 +8,3 @@ Homepage: https://github.com/Akascape/CTkToolTip
__version__ = '0.8'
from .ctk_tooltip import CTkToolTip
-
diff --git a/CTkToolTip/ctk_tooltip.py b/boiiiwd_package/src/CTkToolTip/ctk_tooltip.py
similarity index 100%
rename from CTkToolTip/ctk_tooltip.py
rename to boiiiwd_package/src/CTkToolTip/ctk_tooltip.py
diff --git a/boiiiwd_package/src/__init__.py b/boiiiwd_package/src/__init__.py
index e69de29..d8eeb44 100644
--- a/boiiiwd_package/src/__init__.py
+++ b/boiiiwd_package/src/__init__.py
@@ -0,0 +1,5 @@
+import os, glob
+from os.path import dirname, basename, isfile, join
+
+modules = glob.glob(os.path.join(dirname(__file__), '*.py'))
+__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]
\ No newline at end of file
diff --git a/boiiiwd_package/src/helpers.py b/boiiiwd_package/src/helpers.py
index 694790d..ce4670e 100644
--- a/boiiiwd_package/src/helpers.py
+++ b/boiiiwd_package/src/helpers.py
@@ -1,18 +1,20 @@
+import src.shared_vars as main_app
from src.imports import *
# Start helper functions
-def cwd():
- if getattr(sys, 'frozen', False):
- return os.path.dirname(sys.executable)
- else:
- return os.path.dirname(os.path.abspath(__file__))
+
+#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)
if fallback:
return config.get("Settings", name, fallback=fallback)
- return config.get("Settings", name, fallback="on")
+ return config.get("Settings", name, fallback="")
def save_config(name, value):
config = configparser.ConfigParser()
@@ -23,13 +25,11 @@ def save_config(name, value):
config.write(config_file)
def check_custom_theme(theme_name):
- if os.path.exists(os.path.join(cwd(), theme_name)):
- return os.path.join(cwd(), 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, "boiiiwd_theme.json")
+ try: return os.path.join(RESOURCES_DIR, theme_name)
+ except: return os.path.join(RESOURCES_DIR, "boiiiwd_theme.json")
# theme initialization
ctk.set_appearance_mode(check_config("appearance", "Dark")) # Modes: "System" (standard), "Dark", "Light"
@@ -75,6 +75,64 @@ def create_update_script(current_exe, new_exe, updater_folder, program_name):
return script_path
+def if_internet_available(func):
+ if func == "return":
+ try:
+ requests.get("https://www.google.com", timeout=3)
+ 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.")
+ 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(".", ""))
+
+ 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)
+
+ result = msg_box.get()
+
+ if result == "View":
+ webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest")
+
+ if result == "Yes":
+ from src.update_window import UpdateWindow
+ update_window = UpdateWindow(window, LATEST_RELEASE_URL)
+ update_window.start_update()
+
+ if result == "No":
+ return
+
+ elif int_latest_version < int_current_version:
+ if ignore_up_todate:
+ return
+ msg_box = CTkMessagebox(title="Up to Date!", message=f"Unreleased version!\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="Ok", sound=True)
+ result = msg_box.get()
+ 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)
+ result = msg_box.get()
+
+ else:
+ 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+)'
@@ -111,6 +169,7 @@ def initialize_steam(master):
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):
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
response = requests.get(url)
@@ -141,7 +200,7 @@ def convert_speed(speed_bytes):
def create_default_config():
config = configparser.ConfigParser()
config["Settings"] = {
- "SteamCMDPath": cwd(),
+ "SteamCMDPath": application_path,
"DestinationFolder": "",
"checkforupdtes": "on",
"console": "off"
@@ -152,7 +211,7 @@ def create_default_config():
def get_steamcmd_path():
config = configparser.ConfigParser()
config.read(CONFIG_FILE_PATH)
- return config.get("Settings", "SteamCMDPath", fallback=cwd())
+ return config.get("Settings", "SteamCMDPath", fallback=application_path)
def extract_json_data(json_path, key):
with open(json_path, 'r') as json_file:
@@ -204,8 +263,7 @@ def show_message(title, message, icon="warning", _return=False, option_1="No", o
else:
def callback():
CTkMessagebox(title=title, message=message, icon=icon, sound=True)
- from src.main import master_win
- master_win.after(0, callback)
+ main_app.app.after(0, callback)
def launch_boiii_func(path):
procname = "boiii.exe"
@@ -271,11 +329,22 @@ def get_button_state_colors(file_path, state):
def reset_steamcmd(no_warn=None):
steamcmd_path = get_steamcmd_path()
- steamcmd_steamapps = os.path.join(steamcmd_path, "steamapps")
- if os.path.exists(steamcmd_steamapps):
- remove_tree(steamcmd_steamapps, show_error=True)
- if not no_warn:
- show_message("Success!", "SteamCMD has been reset successfully!", icon="info")
+
+ directories_to_reset = ["steamapps", "dumps", "logs", "depotcache", "appcache","userdata",]
+
+ for directory in directories_to_reset:
+ directory_path = os.path.join(steamcmd_path, directory)
+ if os.path.exists(directory_path):
+ remove_tree(directory_path, show_error=True)
+
+ for root, _, files in os.walk(steamcmd_path):
+ for filename in files:
+ if filename.endswith((".old", ".crash")):
+ file_path = os.path.join(root, filename)
+ os.remove(file_path)
+
+ if not no_warn:
+ 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")
@@ -298,7 +367,30 @@ def get_item_name(id):
except:
return False
-def show_noti(widget ,message, event=None, noti_dur=3.0):
- CTkToolTip(widget, message=message, is_noti=True, noti_event=event, noti_dur=noti_dur)
+# 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 check_item_date(down_date, date_updated):
+ 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"
+ try:
+ try:
+ 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)
+
+ try:
+ upload_datetime = datetime.strptime(date_updated, date_format_with_year)
+ except ValueError:
+ upload_datetime = datetime.strptime(date_updated + f", {current_year}", date_format_with_added_year)
+
+ if upload_datetime >= download_datetime:
+ return True
+ elif upload_datetime < download_datetime:
+ return False
+ except:
+ return False
# End helper functions
diff --git a/boiiiwd_package/src/imports.py b/boiiiwd_package/src/imports.py
index 954a33c..02bb496 100644
--- a/boiiiwd_package/src/imports.py
+++ b/boiiiwd_package/src/imports.py
@@ -1,30 +1,44 @@
-from CTkMessagebox import CTkMessagebox
-from tkinter import Menu, END, Event
-from bs4 import BeautifulSoup
-import customtkinter as ctk
-from CTkToolTip import *
-from pathlib import Path
-from PIL import Image
import configparser
-import webbrowser
-import subprocess
-import threading
-import datetime
-import requests
-import zipfile
-import shutil
-import psutil
+import io
import json
import math
-import time
-import sys
-import io
import os
import re
+import shutil
+import subprocess
+import sys
+import threading
+import time
+import webbrowser
+import zipfile
+from datetime import datetime
+from pathlib import Path
+from tkinter import END, Event, Menu
-VERSION = "v0.2.9"
+import customtkinter as ctk
+import psutil
+import requests
+from bs4 import BeautifulSoup
+from CTkMessagebox import CTkMessagebox
+from PIL import Image
+
+# Use CTkToolTip and CTkListbox from my repo originally by Akascape (https://github.com/Akascape)
+from .CTkListbox.ctk_listbox import CTkListbox
+from .CTkToolTip.ctk_tooltip import CTkToolTip
+
+
+if getattr(sys, 'frozen', False):
+ # If the application is run as a bundle, the PyInstaller bootloader
+ # extends the sys module by a flag frozen=True and sets the app
+ # path into variable _MEIPASS'.
+ application_path = os.path.dirname(sys.executable)
+else:
+ application_path = os.path.dirname(os.path.abspath(__file__))
+
+CONFIG_FILE_PATH = "config.ini"
GITHUB_REPO = "faroukbmiled/BOIIIWD"
LATEST_RELEASE_URL = "https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip"
+LIBRARY_FILE = "boiiiwd_library.json"
+RESOURCES_DIR = os.path.join(os.path.dirname(__file__), '..', 'resources')
UPDATER_FOLDER = "update"
-CONFIG_FILE_PATH = "config.ini"
-RESOURCES_DIR = os.path.join(os.path.dirname(__file__), 'resources')
\ No newline at end of file
+VERSION = "v0.3.1"
\ No newline at end of file
diff --git a/boiiiwd_package/src/library_tab.py b/boiiiwd_package/src/library_tab.py
index 2d73437..a5644b4 100644
--- a/boiiiwd_package/src/library_tab.py
+++ b/boiiiwd_package/src/library_tab.py
@@ -1,12 +1,15 @@
from src.imports import *
-from src.helpers import show_message, get_folder_size, convert_bytes_to_readable,\
- extract_json_data, get_workshop_file_size, extract_workshop_id, show_noti
+from src.helpers import *
+
+import src.shared_vars as main_app
+
class LibraryTab(ctk.CTkScrollableFrame):
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.added_items = set()
+ self.to_update = set()
self.grid_columnconfigure(0, weight=1)
self.radiobutton_variable = ctk.StringVar()
@@ -15,21 +18,32 @@ class LibraryTab(ctk.CTkScrollableFrame):
self.filter_entry.bind("", self.filter_items)
self.filter_entry.grid(row=0, column=0, padx=(10, 20), pady=(10, 20), sticky="we")
filter_refresh_button_image = os.path.join(RESOURCES_DIR, "Refresh_icon.svg.png")
+ update_button_image = os.path.join(RESOURCES_DIR, "update_icon.png")
self.filter_refresh_button = ctk.CTkButton(self, image=ctk.CTkImage(Image.open(filter_refresh_button_image)), command=self.refresh_items, width=20, height=20,
fg_color="transparent", text="")
- self.filter_refresh_button.grid(row=0, column=1, padx=(10, 20), pady=(10, 20), sticky="enw")
+ self.filter_refresh_button.grid(row=0, column=1, padx=(10, 0), pady=(10, 20), sticky="nw")
+ self.update_button = ctk.CTkButton(self, image=ctk.CTkImage(Image.open(update_button_image)), command=self.check_for_updates, width=65, height=20,
+ text="", fg_color="transparent")
+ self.update_button.grid(row=0, column=1, padx=(0, 20), pady=(10, 20), sticky="en")
+ self.update_tooltip = CTkToolTip(self.update_button, message="Check items for updates", topmost=True)
+ filter_tooltip = CTkToolTip(self.filter_refresh_button, message="Refresh library", topmost=True)
self.label_list = []
self.button_list = []
self.button_view_list = []
+ self.file_cleaned = False
self.filter_type = True
self.clipboard_has_content = False
+ self.item_block_list = set()
+ self.added_folders = set()
+ self.ids_added = set()
+ self.refresh_next_time = False
- def add_item(self, item, image=None, item_type="map", workshop_id=None, folder=None):
+ def add_item(self, item, image=None, workshop_id=None, folder=None, invalid_warn=False):
label = ctk.CTkLabel(self, text=item, image=image, compound="left", padx=5, anchor="w")
button = ctk.CTkButton(self, text="Remove", width=60, height=24, fg_color="#3d3f42")
button_view = ctk.CTkButton(self, text="Details", width=55, height=24, fg_color="#3d3f42")
- button.configure(command=lambda: self.remove_item(item, folder))
- button_view.configure(command=lambda: self.show_map_info(workshop_id))
+ button.configure(command=lambda: self.remove_item(item, folder, workshop_id))
+ button_view.configure(command=lambda: self.show_map_info(workshop_id, folder ,invalid_warn))
button_view_tooltip = CTkToolTip(button_view, message="Opens up a window that shows basic details")
button_tooltip = CTkToolTip(button, message="Removes the map/mod from your game")
label.grid(row=len(self.label_list) + 1, column=0, pady=(0, 10), padx=(5, 10), sticky="w")
@@ -44,6 +58,8 @@ class LibraryTab(ctk.CTkScrollableFrame):
label.bind("", lambda event, label=label: self.copy_to_clipboard(label, workshop_id, event, append=True))
label.bind("", lambda event: self.open_folder_location(folder, event))
label.bind("", lambda event, label=label: self.copy_to_clipboard(label, folder, event))
+ if invalid_warn:
+ label_warn = CTkToolTip(label, message="Duplicated or Blocked item (Search item id in search)")
def on_label_hover(self, label, enter):
if enter:
@@ -75,6 +91,98 @@ class LibraryTab(ctk.CTkScrollableFrame):
os.startfile(folder)
show_noti(self, "Opening folder", event, 1.0)
+ def item_exists_in_file(self, items_file, workshop_id, folder_name=None):
+ if not os.path.exists(items_file):
+ return False, False
+
+ with open(items_file, "r") as f:
+ items_data = json.load(f)
+ for item_info in items_data:
+ if "id" in item_info and "folder_name" in item_info and "json_folder_name" in item_info:
+ if item_info["id"] == workshop_id and item_info["folder_name"] == folder_name:
+ if item_info["folder_name"] in self.added_folders:
+ continue
+ if item_info["folder_name"] in self.item_block_list:
+ return False ,None
+ return True, True
+ elif item_info["id"] == workshop_id:
+ if item_info["folder_name"] in self.added_folders:
+ continue
+ if item_info["folder_name"] in self.item_block_list:
+ return False ,None
+ return True, False
+
+ elif "id" in item_info and item_info["id"] == workshop_id:
+ return True, False
+ return False, False
+
+ def remove_item_by_option(self, items_file, option, option_name="id"):
+
+ if not os.path.exists(items_file):
+ return
+
+ with open(items_file, "r") as f:
+ items_data = json.load(f)
+
+ updated_items_data = [item for item in items_data if item.get(option_name) != option]
+
+ if len(updated_items_data) < len(items_data):
+ with open(items_file, "w") as f:
+ json.dump(updated_items_data, f, indent=4)
+
+ def get_item_by_id(self, items_file, item_id, return_option="all"):
+
+ if not os.path.exists(items_file):
+ return None
+
+ with open(items_file, "r") as f:
+ items_data = json.load(f)
+
+ for item in items_data:
+ if item.get("id") == item_id:
+ if return_option == "all":
+ return item
+ elif return_option == return_option:
+ return item.get(return_option)
+ return None
+
+ def get_item_index_by_id(self, items_data, item_id):
+ for index, item in enumerate(items_data):
+ if item.get("id") == item_id:
+ return index
+ return None
+
+ def update_or_add_item_by_id(self, items_file, item_info, item_id):
+ if not os.path.exists(items_file):
+ with open(items_file, "w") as f:
+ json.dump([item_info], f, indent=4)
+ else:
+ with open(items_file, "r+") as f:
+ items_data = json.load(f)
+ existing_item_index = self.get_item_index_by_id(items_data, item_id)
+ if existing_item_index is not None:
+ items_data[existing_item_index] = item_info
+ else:
+ items_data.append(item_info)
+ f.seek(0)
+ f.truncate()
+ json.dump(items_data, f, indent=4)
+
+ def clean_json_file(self, file):
+
+ if not os.path.exists(file):
+ show_message("Error", f"File '{file}' does not exist.")
+ return
+
+ with open(file, "r") as f:
+ items_data = json.load(f)
+
+ cleaned_items = [item for item in items_data if 'folder_name' in item and 'json_folder_name'
+ in item and item['folder_name'] not in self.item_block_list and item['folder_name'] in self.added_folders]
+
+ with open(file, 'w') as file:
+ json.dump(cleaned_items, file, indent=4)
+
def filter_items(self, event):
filter_text = self.filter_entry.get().lower()
for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list):
@@ -88,43 +196,186 @@ class LibraryTab(ctk.CTkScrollableFrame):
button_view_list.grid_remove()
button.grid_remove()
- def load_items(self, boiiiFolder):
+ def load_items(self, boiiiFolder, dont_add=False):
+ if self.refresh_next_time and not dont_add:
+ self.refresh_next_time = False
+ self.refresh_items()
+
+ if dont_add:
+ self.refresh_next_time = True
+
maps_folder = Path(boiiiFolder) / "mods"
mods_folder = Path(boiiiFolder) / "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")
+ b_map_img = os.path.join(RESOURCES_DIR, "b_map_image.png")
+ map_count = 0
+ mod_count = 0
+ total_size = 0
folders_to_process = [mods_folder, maps_folder]
+ items_file = os.path.join(application_path, LIBRARY_FILE)
+
for folder_path in folders_to_process:
for zone_path in folder_path.glob("**/zone"):
json_path = zone_path / "workshop.json"
if json_path.exists():
- name = extract_json_data(json_path, "Title").replace(">", "").replace("^", "")
+
+ curr_folder_name = zone_path.parent.name
+ workshop_id = extract_json_data(json_path, "PublisherID") or "None"
+ name = re.sub(r'\^\w+', '', extract_json_data(json_path, "Title")) or "None"
name = name[:45] + "..." if len(name) > 45 else name
- item_type = extract_json_data(json_path, "Type")
- workshop_id = extract_json_data(json_path, "PublisherID")
- folder_name = extract_json_data(json_path, "FolderName")
- size = convert_bytes_to_readable(get_folder_size(zone_path.parent))
+ 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)
+ size = convert_bytes_to_readable(folder_size_bytes)
+ total_size += folder_size_bytes
text_to_add = f"{name} | Type: {item_type.capitalize()}"
mode_type = "ZM" if item_type == "map" and folder_name.startswith("zm") else "MP" if folder_name.startswith("mp") and item_type == "map" else None
if mode_type:
text_to_add += f" | Mode: {mode_type}"
text_to_add += f" | ID: {workshop_id} | Size: {size}"
- if text_to_add not in self.added_items:
- self.added_items.add(text_to_add)
- image_path = mod_img if item_type == "mod" else map_img
- self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), item_type=item_type, workshop_id=workshop_id, folder=zone_path.parent)
+ creation_timestamp = None
+ for ff_file in zone_path.glob("*.ff"):
+ if ff_file.exists():
+ creation_timestamp = ff_file.stat().st_ctime
+ break
+
+ if creation_timestamp is not None:
+ date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
+ else:
+ creation_timestamp = zone_path.stat().st_ctime
+ date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
+
+ map_count += 1 if item_type == "map" else 0
+ mod_count += 1 if item_type == "mod" else 0
+ 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}"):
+ try: self.remove_item_by_option(items_file, curr_folder_name, "folder_name")
+ except: pass
+ self.item_block_list.add(curr_folder_name)
+ image_path = b_mod_img if item_type == "mod" else b_map_img
+ text_to_add = re.sub(r'ID:\s+(?:\d+|None)', f'Folder: {curr_folder_name}', text_to_add)
+ text_to_add += " | ⚠️"
+ 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
+ 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 += " | ⚠️"
+
+ self.added_items.add(text_to_add)
+ if image_path is b_mod_img or image_path is b_map_img and not dont_add:
+ self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), workshop_id=workshop_id, folder=zone_path.parent, invalid_warn=True)
+ elif not dont_add:
+ self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), workshop_id=workshop_id, folder=zone_path.parent)
+ id_found, folder_found = self.item_exists_in_file(items_file, workshop_id, curr_folder_name)
+ item_info = {
+ "id": workshop_id,
+ "text": text_to_add,
+ "date": date_added,
+ "folder_name": curr_folder_name,
+ "json_folder_name": folder_name
+ }
+ # 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:
+ if not os.path.exists(items_file):
+ with open(items_file, "w") as f:
+ json.dump([item_info], f, indent=4)
+ else:
+ with open(items_file, "r+") as f:
+ items_data = json.load(f)
+ items_data.append(item_info)
+ 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:
+ 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:
+ self.ids_added.add(workshop_id)
+
+ if not self.file_cleaned and os.path.exists(items_file):
+ self.file_cleaned = True
+ self.clean_json_file(items_file)
if not self.added_items:
self.show_no_items_message()
else:
self.hide_no_items_message()
- def remove_item(self, item, folder):
+ if map_count > 0 or mod_count > 0:
+ return f"Maps: {map_count} - Mods: {mod_count} - Total size: {convert_bytes_to_readable(total_size)}"
+ return "No items in current selected folder"
+
+ def update_item(self, boiiiFolder, id, item_type, foldername):
+ try:
+ if item_type == "map":
+ folder_path = Path(boiiiFolder) / "usermaps" / f"{foldername}"
+ elif item_type == "mod":
+ folder_path = Path(boiiiFolder) / "mods" / f"{foldername}"
+ else:
+ raise ValueError("Unsupported item_type. It must be 'map' or 'mod'.")
+
+ 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
+ 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))
+ text_to_add = f"{name} | Type: {item_type.capitalize()}"
+ mode_type = "ZM" if item_type == "map" and folder_name.startswith("zm") else "MP" if folder_name.startswith("mp") and item_type == "map" else None
+ if mode_type:
+ text_to_add += f" | Mode: {mode_type}"
+ text_to_add += f" | ID: {workshop_id} | Size: {size}"
+
+ creation_timestamp = None
+ for ff_file in zone_path.glob("*.ff"):
+ if ff_file.exists():
+ creation_timestamp = ff_file.stat().st_ctime
+ break
+
+ if creation_timestamp is not None:
+ date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
+ else:
+ creation_timestamp = zone_path.stat().st_ctime
+ date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
+
+ items_file = os.path.join(application_path, LIBRARY_FILE)
+
+ item_info = {
+ "id": workshop_id,
+ "text": text_to_add,
+ "date": date_added,
+ "folder_name": foldername,
+ "json_folder_name": folder_name
+ }
+ self.update_or_add_item_by_id(items_file, item_info, id)
+ return
+
+ except Exception as e:
+ show_message("Error updating json file", f"Error while updating library json file\n{e}")
+
+ def remove_item(self, item, folder, id):
+ items_file = os.path.join(application_path, LIBRARY_FILE)
for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list):
if item == label.cget("text"):
+ self.added_folders.remove(os.path.basename(folder))
try:
shutil.rmtree(folder)
except Exception as e:
@@ -136,9 +387,12 @@ class LibraryTab(ctk.CTkScrollableFrame):
self.label_list.remove(label)
self.button_list.remove(button)
self.added_items.remove(label.cget("text"))
+ self.ids_added.remove(id)
self.button_view_list.remove(button_view_list)
+ self.remove_item_by_option(items_file, id)
- def refresh_items(self):
+ def refresh_items(self, skip_load=False):
+ main_app.app.title("BOIII Workshop Downloader - Library ➜ Loading... ⏳")
for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list):
label.destroy()
button.destroy()
@@ -147,8 +401,11 @@ class LibraryTab(ctk.CTkScrollableFrame):
self.button_list.clear()
self.button_view_list.clear()
self.added_items.clear()
- from src.main import master_win
- self.load_items(master_win.edit_destination_folder.get().strip())
+ self.added_folders.clear()
+ self.ids_added.clear()
+ if not skip_load:
+ status = self.load_items(main_app.app.edit_destination_folder.get().strip())
+ main_app.app.title(f"BOIII Workshop Downloader - Library ➜ {status}")
def view_item(self, workshop_id):
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
@@ -162,99 +419,159 @@ class LibraryTab(ctk.CTkScrollableFrame):
self.no_items_label.configure(text="")
self.no_items_label.forget()
- # i know i know ,please make a pull request i cant be bother
- def show_map_info(self, workshop):
+ def show_map_info(self, workshop, folder, invalid_warn=False):
for button_view in self.button_view_list:
button_view.configure(state="disabled")
def show_map_thread():
workshop_id = workshop
+ online = if_internet_available("return")
+ valid_id = None
if not workshop_id.isdigit():
try:
if extract_workshop_id(workshop_id).strip().isdigit():
workshop_id = extract_workshop_id(workshop_id).strip()
else:
- show_message("Warning", "Not a valid Workshop ID.")
+ raise
except:
- show_message("Warning", "Not a valid Workshop ID.")
- return
- try:
- url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
- response = requests.get(url)
- response.raise_for_status()
- content = response.text
-
- soup = BeautifulSoup(content, "html.parser")
-
- try:
- map_mod_type = soup.find("div", class_="rightDetailsBlock").text.strip()
- map_name = soup.find("div", class_="workshopItemTitle").text.strip()
- map_size = map_size = get_workshop_file_size(workshop_id, raw=True)
- details_stats_container = soup.find("div", class_="detailsStatsContainerRight")
- details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight")
- date_created = details_stat_elements[1].text.strip()
- try:
- ratings = soup.find('div', class_='numRatings')
- ratings_text = ratings.get_text()
- except:
- ratings = "Not found"
- ratings_text= "Not enough ratings"
- try:
- date_updated = details_stat_elements[2].text.strip()
- except:
- date_updated = "Not updated"
- stars_div = soup.find("div", class_="fileRatingDetails")
- starts = stars_div.find("img")["src"]
- except:
- show_message("Warning", "Not a valid Workshop ID\nCouldn't get information.")
+ valid_id = False
+ # show_message("Warning", "Not a valid Workshop ID.")
for button_view in self.button_view_list:
button_view.configure(state="normal")
- return
-
+ # return
+ if online and valid_id!=False:
try:
- preview_image_element = soup.find("img", id="previewImage")
- workshop_item_image_url = preview_image_element["src"]
- except:
+ url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
+ response = requests.get(url)
+ response.raise_for_status()
+ content = response.text
+
+ soup = BeautifulSoup(content, "html.parser")
+
try:
- preview_image_element = soup.find("img", id="previewImageMain")
- workshop_item_image_url = preview_image_element["src"]
- except Exception as e:
- show_message("Warning", f"Failed to get preview image ,probably wrong link/id if not please open an issue on github.\n{e}")
+ map_mod_type = soup.find("div", class_="rightDetailsBlock").text.strip()
+ map_name = soup.find("div", class_="workshopItemTitle").text.strip()
+ map_size = map_size = get_workshop_file_size(workshop_id, raw=True)
+ details_stats_container = soup.find("div", class_="detailsStatsContainerRight")
+ details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight")
+ date_created = details_stat_elements[1].text.strip()
+ try:
+ ratings = soup.find('div', class_='numRatings')
+ ratings_text = ratings.get_text()
+ except:
+ ratings = "Not found"
+ ratings_text= "Not enough ratings"
+ try:
+ date_updated = details_stat_elements[2].text.strip()
+ except:
+ date_updated = "Not updated"
+ try:
+ description = soup.find('div', class_='workshopItemDescription').get_text(separator='\n')
+ except:
+ description = "Not available"
+
+ stars_div = soup.find("div", class_="fileRatingDetails")
+ starts = stars_div.find("img")["src"]
+ except:
+ show_message("Warning", "Couldn't get information.")
for button_view in self.button_view_list:
button_view.configure(state="normal")
return
- starts_image_response = requests.get(starts)
- stars_image = Image.open(io.BytesIO(starts_image_response.content))
- stars_image_size = stars_image.size
+ try:
+ preview_image_element = soup.find("img", id="previewImage")
+ workshop_item_image_url = preview_image_element["src"]
+ except:
+ try:
+ preview_image_element = soup.find("img", id="previewImageMain")
+ workshop_item_image_url = preview_image_element["src"]
+ except Exception as e:
+ show_message("Warning", f"Failed to get preview image ,probably wrong link/id if not please open an issue on github.\n{e}")
+ for button_view in self.button_view_list:
+ button_view.configure(state="normal")
+ return
- image_response = requests.get(workshop_item_image_url)
- image_response.raise_for_status()
- image = Image.open(io.BytesIO(image_response.content))
- image_size = image.size
+ starts_image_response = requests.get(starts)
+ stars_image = Image.open(io.BytesIO(starts_image_response.content))
+ stars_image_size = stars_image.size
- self.toplevel_info_window(map_name, map_mod_type, map_size, image, image_size, date_created,
- date_updated, stars_image, stars_image_size, ratings_text, url)
+ image_response = requests.get(workshop_item_image_url)
+ image_response.raise_for_status()
+ image = Image.open(io.BytesIO(image_response.content))
+ image_size = image.size
- except requests.exceptions.RequestException as e:
- show_message("Error", f"Failed to fetch map information.\nError: {e}", icon="cancel")
- for button_view in self.button_view_list:
- button_view.configure(state="normal")
- return
+ self.toplevel_info_window(map_name, map_mod_type, map_size, image, image_size, date_created,
+ date_updated, stars_image, stars_image_size, ratings_text,
+ url, workshop_id, invalid_warn, folder, description, online)
+
+ except Exception as e:
+ show_message("Error", f"Failed to fetch information.\nError: {e}", icon="cancel")
+ for button_view in self.button_view_list:
+ button_view.configure(state="normal")
+ return
+ else:
+ json_path = Path(folder) / "zone" / "workshop.json"
+ creation_timestamp = None
+ for ff_file in json_path.parent.glob("*.ff"):
+ if ff_file.exists():
+ creation_timestamp = ff_file.stat().st_ctime
+ break
+ if not creation_timestamp:
+ creation_timestamp = json_path.parent.stat().st_ctime
+
+ 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_mod_type = extract_json_data(json_path, "Type") or "None"
+ folder_size_bytes = get_folder_size(json_path.parent.parent)
+ map_size = convert_bytes_to_readable(folder_size_bytes)
+ preview_iamge = json_path.parent / "previewimage.png"
+ if preview_iamge.exists():
+ image = Image.open(preview_iamge)
+ else:
+ image = Image.open(os.path.join(RESOURCES_DIR, "ryuk.png"))
+ image_size = image.size
+ offline_date = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
+ date_updated = "Offline"
+ date_created = "Offline"
+ stars_image = Image.open(os.path.join(RESOURCES_DIR, "ryuk.png"))
+ stars_image_size = stars_image.size
+ ratings_text = "Offline"
+ description = re.sub(r'\^\w+', '', extract_json_data(json_path, "Description")) or "Not available"
+ url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
+
+ self.toplevel_info_window(map_name, map_mod_type, map_size, image, image_size, date_created,
+ date_updated, stars_image, stars_image_size, ratings_text,
+ url, workshop_id, invalid_warn, folder, description, online ,offline_date)
+ else:
+ show_message("Warning", "Couldn't get offline information, Please connect to internet and try again")
+ for button_view in self.button_view_list:
+ button_view.configure(state="normal")
+ return
info_thread = threading.Thread(target=show_map_thread)
info_thread.start()
def toplevel_info_window(self, map_name, map_mod_type, map_size, image, image_size,
- date_created ,date_updated, stars_image, stars_image_size, ratings_text, url):
+ date_created ,date_updated, stars_image, stars_image_size, ratings_text,
+ url, workshop_id, invalid_warn, folder, description ,online,offline_date=None):
def main_thread():
try:
+ items_file = os.path.join(application_path, LIBRARY_FILE)
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("Map/Mod Information")
top.attributes('-topmost', 'true')
+ size_text = "Size (Workshop):"
+
+ if offline_date:
+ down_date = offline_date
+ size_text = "Size (On Disk):"
+ else:
+ down_date = self.get_item_by_id(items_file, workshop_id, 'date')
def close_window():
top.destroy()
@@ -262,6 +579,53 @@ class LibraryTab(ctk.CTkScrollableFrame):
def view_map_mod():
webbrowser.open(url)
+ def show_description(event):
+ def main_thread():
+ description_window = ctk.CTkToplevel(None)
+
+ if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")):
+ description_window.after(210, lambda: description_window.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")))
+
+ description_window.attributes('-topmost', 'true')
+ description_window.title(f"Description - {map_name}")
+ x_pos = event.x_root - 300
+ y_pos = event.y_root - 200
+ calc_req_width = len(description) * 6 + 5
+ win_width = calc_req_width if calc_req_width < 500 else 500
+ description_window.geometry(f"{win_width + 5}x300+{x_pos}+{y_pos}")
+
+ if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_obsidian.json":
+ description_label = ctk.CTkTextbox(description_window, activate_scrollbars=True, scrollbar_button_color="#5b6c7f")
+ else:
+ description_label = ctk.CTkTextbox(description_window, activate_scrollbars=True)
+ description_label.insert("1.0", description)
+ description_label.pack(fill=ctk.BOTH, expand=True, padx=(10, 10), pady=(10, 10))
+ description_label.configure(state="disabled")
+
+ main_app.app.create_context_menu(description_label, textbox=True)
+ description_window.after(50, description_window.focus_set)
+
+ main_app.app.after(0, main_thread)
+
+ def check_for_updates():
+ try:
+
+ if check_item_date(down_date, date_updated):
+ if show_message("There is an update.", "Press download to redownload!", icon="info", _return=True, option_1="No", option_2="Download"):
+ if main_app.app.is_downloading:
+ show_message("Error", "Please wait for the current download to finish or stop it then restart.", icon="cancel")
+ return
+ 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()
+ main_app.app.download_map(update=True)
+ top.destroy()
+ return
+ else:
+ show_message("Up to date!", "No updates found!", icon="info")
+ except:
+ show_message("Up to date!", "No updates found!", icon="info")
+
# frames
stars_frame = ctk.CTkFrame(top)
stars_frame.grid(row=0, column=0, columnspan=2, padx=20, pady=(20, 0), sticky="nsew")
@@ -281,17 +645,42 @@ class LibraryTab(ctk.CTkScrollableFrame):
name_label = ctk.CTkLabel(info_frame, text=f"Name: {map_name}")
name_label.grid(row=0, column=0, columnspan=2, sticky="w", padx=20, pady=5)
- type_label = ctk.CTkLabel(info_frame, text=f"Type: {map_mod_type}")
- type_label.grid(row=1, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ desc_threshold = 30
+ shortened_description = re.sub(r'\n', '', description).strip()
+ shortened_description = re.sub(r'([^a-zA-Z0-9\s:().])', '', shortened_description)
+ shortened_description = f"{shortened_description[:desc_threshold]}... (View)"\
+ if len(shortened_description) > desc_threshold else shortened_description
+ description_lab = ctk.CTkLabel(info_frame, text=f"Description: {shortened_description}")
+ description_lab.grid(row=1, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ if len(description) > desc_threshold:
+ description_lab_tooltip = CTkToolTip(description_lab, message="View description", topmost=True)
+ description_lab.configure(cursor="hand2")
+ description_lab.bind("", lambda e: show_description(e))
- size_label = ctk.CTkLabel(info_frame, text=f"Size (Workshop): {map_size}")
- size_label.grid(row=2, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ id_label = ctk.CTkLabel(info_frame, text=f"ID: {workshop_id} | Folder: {os.path.basename(folder)}")
+ id_label.grid(row=2, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+
+ type_label = ctk.CTkLabel(info_frame, text=f"Type: {map_mod_type}")
+ type_label.grid(row=3, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+
+ size_label = ctk.CTkLabel(info_frame, text=f"{size_text} {map_size}")
+ size_label.grid(row=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
date_created_label = ctk.CTkLabel(info_frame, text=f"Posted: {date_created}")
- date_created_label.grid(row=3, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ date_created_label.grid(row=5, column=0, columnspan=2, sticky="w", padx=20, pady=5)
- date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated}")
- date_updated_label.grid(row=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ if date_updated != "Not updated" and date_updated != "Offline":
+ date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated} 🔗")
+ date_updated_label_tooltip = CTkToolTip(date_updated_label, message="View changelogs", topmost=True)
+ date_updated_label.configure(cursor="hand2")
+ date_updated_label.bind("", lambda e:
+ webbrowser.open(f"https://steamcommunity.com/sharedfiles/filedetails/changelog/{workshop_id}"))
+ else:
+ date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated}")
+ date_updated_label.grid(row=6, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+
+ date_updated_label = ctk.CTkLabel(info_frame, text=f"Downloaded at: {down_date}")
+ date_updated_label.grid(row=7, column=0, columnspan=2, sticky="w", padx=20, pady=5)
stars_image_label = ctk.CTkLabel(stars_frame)
stars_width, stars_height = stars_image_size
@@ -305,16 +694,39 @@ class LibraryTab(ctk.CTkScrollableFrame):
image_label = ctk.CTkLabel(image_frame)
width, height = image_size
+ # preview image is too big if offline, // to round floats
+ if width > 1000 or height > 1000:
+ width = width // 2
+ height = height // 2
+
image_widget = ctk.CTkImage(image, size=(int(width), int(height)))
image_label.configure(image=image_widget, text="")
image_label.pack(expand=True, fill="both", padx=(10, 20), pady=(10, 10))
# Buttons
- close_button = ctk.CTkButton(buttons_frame, text="View", command=view_map_mod)
- close_button.pack(side="left", padx=(10, 20), pady=(10, 10))
+ view_button = ctk.CTkButton(buttons_frame, text="View", command=view_map_mod, width=130)
+ view_button.grid(row=0, column=0, padx=(20, 20), pady=(10, 10), sticky="n")
+ view_button_tooltip = CTkToolTip(view_button, message="View Workshop", topmost=True)
- view_button = ctk.CTkButton(buttons_frame, text="Close", command=close_window)
- view_button.pack(side="right", padx=(10, 20), pady=(10, 10))
+ update_btn = ctk.CTkButton(buttons_frame, text="Update", command=check_for_updates, width=130)
+ update_btn.grid(row=0, column=1, padx=(10, 20), pady=(10, 10), sticky="n")
+ update_btn_tooltip = CTkToolTip(update_btn, message="Checks and installs updates of the current selected item (redownload!)", topmost=True)
+
+ close_button = ctk.CTkButton(buttons_frame, text="Close", command=close_window, width=130)
+ close_button.grid(row=0, column=2, padx=(10, 20), pady=(10, 10), sticky="n")
+
+ if not online:
+ view_button.configure(state="disabled")
+ update_btn.configure(state="disabled")
+ update_btn_tooltip.configure(message="Currently offline")
+ view_button_tooltip.configure(message="Currently offline")
+ if invalid_warn:
+ update_btn.configure(text="Update", state="disabled")
+ update_btn_tooltip.configure(message="Disabled due to item being blocked or duplicated")
+
+ if not workshop_id.isdigit():
+ view_button.configure(text="View", state="disabled")
+ view_button_tooltip.configure(message="Not a valid Workshop ID")
top.grid_rowconfigure(0, weight=0)
top.grid_rowconfigure(1, weight=0)
@@ -322,7 +734,203 @@ class LibraryTab(ctk.CTkScrollableFrame):
top.grid_columnconfigure(0, weight=1)
top.grid_columnconfigure(1, weight=1)
+ buttons_frame.grid_rowconfigure(0, weight=1)
+ buttons_frame.grid_rowconfigure(1, weight=1)
+ buttons_frame.grid_rowconfigure(2, weight=1)
+ buttons_frame.grid_columnconfigure(0, weight=1)
+ buttons_frame.grid_columnconfigure(1, weight=1)
+ buttons_frame.grid_columnconfigure(2, weight=1)
+
finally:
for button_view in self.button_view_list:
button_view.configure(state="normal")
self.after(0, main_thread)
+
+ @if_internet_available
+ def check_for_updates(self, on_launch=False):
+ self.after(1, self.update_button.configure(state="disabled"))
+ self.update_tooltip.configure(message='Still loading please wait...')
+ cevent = Event()
+ cevent.x_root = self.update_button.winfo_rootx()
+ cevent.y_root = self.update_button.winfo_rooty()
+ if not on_launch:
+ show_noti(self.update_button, "Please wait, window will popup shortly", event=cevent, noti_dur=3.0, topmost=True)
+ threading.Thread(target=self.check_items_func, args=(on_launch,)).start()
+
+ def items_update_message(self, to_update_len):
+ def main_thread():
+ if show_message(f"{to_update_len} Item updates available", f"{to_update_len} Workshop Items have an update, Would you like to open the item updater window?", icon="info", _return=True):
+ main_app.app.after(1, self.update_items_window)
+ else: return
+ main_app.app.after(0, main_thread)
+ self.update_button.configure(state="normal", width=65, height=20)
+ self.update_tooltip.configure(message='Check items for updates')
+ return
+
+ def check_items_func(self, on_launch):
+ # Needed to refresh item that needs updates
+ self.to_update.clear()
+
+ def if_id_needs_update(item_id, item_date, text):
+ try:
+ headers = {'Cache-Control': 'no-cache'}
+ url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={item_id}"
+ response = requests.get(url, headers=headers)
+ response.raise_for_status()
+ content = response.text
+ soup = BeautifulSoup(content, "html.parser")
+ details_stats_container = soup.find("div", class_="detailsStatsContainerRight")
+ details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight")
+ try:
+ date_updated = details_stat_elements[2].text.strip()
+ except:
+ try:
+ date_updated = details_stat_elements[1].text.strip()
+ except:
+ return False
+
+ if check_item_date(item_date, date_updated):
+ self.to_update.add(text + f" | Updated: {date_updated}")
+ return True
+ else:
+ return False
+
+ except Exception as e:
+ show_message("Error", f"Error occured\n{e}", icon="cancel")
+ return
+
+ def check_for_update():
+ 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 boiii path!, you also need to have at lease 1 item!")
+ return
+
+ with open(LIBRARY_FILE, 'r') as file:
+ lib_data = json.load(file)
+
+ for item in lib_data:
+ item_id = item["id"]
+ item_date = item["date"]
+ if_id_needs_update(item_id, item_date, item["text"])
+
+ check_for_update()
+
+ to_update_len = len(self.to_update)
+ if to_update_len > 0:
+ self.items_update_message(to_update_len)
+ else:
+ self.update_button.configure(state="normal", width=65, height=20)
+ self.update_tooltip.configure(message='Check items for updates')
+ if not on_launch:
+ show_message("No updates found!", "Items are up to date!", icon="info")
+
+ def update_items_window(self):
+ try:
+ top = ctk.CTkToplevel(master=None)
+ top.withdraw()
+ 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("Item updater - List of Items with Updates - Click to select 1 or more")
+ longest_text_length = max(len(text) for text in self.to_update)
+ window_width = longest_text_length * 6 + 5
+ top.geometry(f"{window_width}x450")
+ top.attributes('-topmost', 'true')
+ top.resizable(True, True)
+ selected_id_list = []
+ cevent = Event()
+ self.select_all_bool = False
+
+ listbox = CTkListbox(top, multiple_selection=True)
+ listbox.grid(row=0, column=0, sticky="nsew")
+
+ update_button = ctk.CTkButton(top, text="Update")
+ update_button.grid(row=1, column=0, pady=10, padx=5, sticky='ns')
+
+ select_button = ctk.CTkButton(top, text="Select All", width=5)
+ select_button.grid(row=1, column=0, pady=10, padx=(230, 0), sticky='ns')
+
+ def open_url(id_part, e=None):
+ url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={id_part}"
+ webbrowser.open(url)
+
+ # you gotta use my modded CTkListbox originaly by Akascape
+ def add_checkbox_item(index, item_text):
+ parts = item_text.split('ID: ')
+ id_part = parts[1].split('|')[0].strip()
+ listbox.insert(index, item_text, keybind="", func=lambda e: open_url(id_part))
+
+ def load_items():
+ for index, item_text in enumerate(self.to_update):
+ if index == len(self.to_update) - 1:
+ add_checkbox_item("end", item_text)
+ top.deiconify()
+ return
+ add_checkbox_item(index, item_text)
+
+ def update_list(selected_option):
+ selected_id_list.clear()
+
+ if selected_option:
+ for option in selected_option:
+ parts = option.split('ID: ')
+ if len(parts) > 1:
+ id_part = parts[1].split('|')[0].strip()
+ selected_id_list.append(id_part)
+
+ def select_all():
+ if self.select_all_bool:
+ listbox.deactivate("all")
+ update_list(listbox.get())
+ self.select_all_bool = False
+ return
+ listbox.deactivate("all")
+ listbox.activate("all")
+ update_list(listbox.get())
+ self.select_all_bool = True
+
+ def update_btn_fun():
+ if len(selected_id_list) == 1:
+ if main_app.app.is_downloading:
+ show_message("Error", "Please wait for the current download to finish or stop it then start.", icon="cancel")
+ return
+ main_app.app.edit_workshop_id.delete(0, "end")
+ main_app.app.edit_workshop_id.insert(0, selected_id_list[0])
+ main_app.app.main_button_event()
+ main_app.app.download_map(update=True)
+ top.destroy()
+ return
+
+ elif len(selected_id_list) > 1:
+ if main_app.app.is_downloading:
+ show_message("Error", "Please wait for the current download to finish or stop it then start.", icon="cancel")
+ return
+ comma_separated_ids = ",".join(selected_id_list)
+ main_app.app.queuetextarea.delete("1.0", "end")
+ main_app.app.queuetextarea.insert("1.0", comma_separated_ids)
+ main_app.app.queue_button_event()
+ main_app.app.download_map(update=True)
+ top.destroy()
+ return
+
+ else:
+ cevent.x_root = update_button.winfo_rootx()
+ cevent.y_root = update_button.winfo_rooty()
+ show_noti(update_button ,"Please select 1 or more items", event=cevent, noti_dur=0.8, topmost=True)
+
+
+ listbox.configure(command=update_list)
+ update_button.configure(command=update_btn_fun)
+ select_button.configure(command=select_all)
+
+ top.grid_rowconfigure(0, weight=1)
+ top.grid_columnconfigure(0, weight=1)
+
+ load_items()
+
+ except Exception as e:
+ show_message("Error", f"{e}", icon="cancel")
+
+ finally:
+ self.update_button.configure(state="normal", width=65, height=20)
+ self.update_tooltip.configure(message='Check items for updates')
diff --git a/boiiiwd_package/src/main.py b/boiiiwd_package/src/main.py
index 4f59478..41c8978 100644
--- a/boiiiwd_package/src/main.py
+++ b/boiiiwd_package/src/main.py
@@ -1,62 +1,21 @@
-from src.imports import *
-from src.helpers import show_message, cwd, check_config, check_custom_theme, get_button_state_colors, convert_speed, valid_id,\
- save_config, check_steamcmd, is_steamcmd_initialized, get_steamcmd_path, reset_steamcmd, get_item_name, get_latest_release_version,\
- get_workshop_file_size, extract_workshop_id, create_default_config, initialize_steam, launch_boiii_func, remove_tree, extract_json_data, convert_seconds, convert_bytes_to_readable
-from src.settings_tab import SettingsTab
+from turtle import title
+from src.update_window import check_for_updates_func
+from src.helpers import *
+
from src.library_tab import LibraryTab
+from src.settings_tab import SettingsTab
-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(".", ""))
-
- 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)
-
- result = msg_box.get()
-
- if result == "View":
- webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest")
-
- from src.update_window import UpdateWindow
- if result == "Yes":
- update_window = UpdateWindow(window, LATEST_RELEASE_URL)
- update_window.start_update()
-
- if result == "No":
- return
-
- elif int_latest_version < int_current_version:
- if ignore_up_todate:
- return
- msg_box = CTkMessagebox(title="Up to Date!", message=f"Unreleased version!\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="Ok", sound=True)
- result = msg_box.get()
- 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)
- result = msg_box.get()
-
- else:
- 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")
class BOIIIWD(ctk.CTk):
def __init__(self):
super().__init__()
- global master_win
- master_win = self
# self.app_instance = BOIIIWD()
# configure window
self.title("BOIII Workshop Downloader - Main")
try:
- geometry_file = os.path.join(cwd(), "boiiiwd_dont_touch.conf")
+ geometry_file = os.path.join(application_path, "boiiiwd_dont_touch.conf")
if os.path.isfile(geometry_file):
with open(geometry_file, "r") as conf:
self.geometry(conf.read())
@@ -190,10 +149,10 @@ class BOIIIWD(ctk.CTk):
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 BOIII folder:")
+ self.label_destination_folder = ctk.CTkLabel(master=self.optionsframe, text='Enter Your boiii 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 BOIII Instalation folder")
+ self.edit_destination_folder = ctk.CTkEntry(master=self.optionsframe, placeholder_text="Your boiii Instalation folder")
self.edit_destination_folder.grid(row=4, column=1, padx=20, pady=(0, 25), columnspan=4, sticky="ewn")
self.button_BOIII_browse = ctk.CTkButton(master=self.optionsframe, text="Select", command=self.open_BOIII_browser)
@@ -265,12 +224,15 @@ class BOIIIWD(ctk.CTk):
self.settings_tab.load_settings("reset_on_fail", "10")
self.settings_tab.load_settings("show_fails", "on")
self.settings_tab.load_settings("skip_already_installed", "on")
- except:
- pass
+ except: pass
if not check_steamcmd():
self.show_steam_warning_message()
+ # items check for update, ill change all the variables to work this way at a later date
+ if self.settings_tab.check_items_var.get():
+ self.library_tab.check_for_updates(on_launch=True)
+
def do_popup(self, event, frame):
try: frame.tk_popup(event.x_root, event.y_root)
finally: frame.grab_release()
@@ -405,9 +367,10 @@ class BOIIIWD(ctk.CTk):
self.library_tab.grid_remove()
def show_library_widgets(self):
- self.title("BOIII Workshop Downloader - Library")
- self.library_tab.load_items(self.edit_destination_folder.get())
+ self.title("BOIII Workshop Downloader - Library ➜ Loading... ⏳")
+ status = self.library_tab.load_items(self.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"BOIII Workshop Downloader - Library ➜ {status}")
def show_queue_widgets(self):
self.title("BOIII Workshop Downloader - Queue")
@@ -462,7 +425,7 @@ class BOIIIWD(ctk.CTk):
def load_configs(self):
if os.path.exists(CONFIG_FILE_PATH):
destination_folder = check_config("DestinationFolder", "")
- steamcmd_path = check_config("SteamCMDPath", os.getcwd())
+ steamcmd_path = check_config("SteamCMDPath", application_path)
new_appearance_mode = check_config("appearance", "Dark")
new_scaling = check_config("scaling", 1.0)
self.edit_destination_folder.delete(0, "end")
@@ -485,7 +448,7 @@ class BOIIIWD(ctk.CTk):
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, cwd())
+ self.edit_steamcmd_path.insert(0, application_path)
create_default_config()
def help_queue_text_func(self, event=None):
@@ -521,7 +484,7 @@ class BOIIIWD(ctk.CTk):
self.queuetextarea.configure(state="disabled")
def open_BOIII_browser(self):
- selected_folder = ctk.filedialog.askdirectory(title="Select BOIII Folder")
+ selected_folder = ctk.filedialog.askdirectory(title="Select boiii Folder")
if selected_folder:
self.edit_destination_folder.delete(0, "end")
self.edit_destination_folder.insert(0, selected_folder)
@@ -551,13 +514,14 @@ class BOIIIWD(ctk.CTk):
link = "https://steamcommunity.com/app/311210/workshop/"
webbrowser.open(link)
+ @if_internet_available
def download_steamcmd(self):
self.edit_steamcmd_path.delete(0, "end")
- self.edit_steamcmd_path.insert(0, cwd())
+ self.edit_steamcmd_path.insert(0, application_path)
save_config("DestinationFolder" ,self.edit_destination_folder.get())
save_config("SteamCMDPath" ,self.edit_steamcmd_path.get())
steamcmd_url = "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"
- steamcmd_zip_path = os.path.join(cwd(), "steamcmd.zip")
+ steamcmd_zip_path = os.path.join(application_path, "steamcmd.zip")
try:
response = requests.get(steamcmd_url)
@@ -567,7 +531,7 @@ class BOIIIWD(ctk.CTk):
zip_file.write(response.content)
with zipfile.ZipFile(steamcmd_zip_path, "r") as zip_ref:
- zip_ref.extractall(cwd())
+ zip_ref.extractall(application_path)
if check_steamcmd():
os.remove(fr"{steamcmd_zip_path}")
@@ -592,6 +556,7 @@ class BOIIIWD(ctk.CTk):
show_message("Error", "Failed to extract SteamCMD. The downloaded file might be corrupted.", icon="cancel")
os.remove(fr"{steamcmd_zip_path}")
+ @if_internet_available
def show_map_info(self):
def show_map_thread():
workshop_id = self.edit_workshop_id.get().strip()
@@ -613,8 +578,9 @@ class BOIIIWD(ctk.CTk):
self.after(1, lambda mid=workshop_id: self.label_file_size.configure(text=f"File size: {get_workshop_file_size(mid ,raw=True)}"))
try:
+ headers = {'Cache-Control': 'no-cache'}
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
- response = requests.get(url)
+ response = requests.get(url, headers=headers)
response.raise_for_status()
content = response.text
@@ -637,6 +603,11 @@ class BOIIIWD(ctk.CTk):
date_updated = details_stat_elements[2].text.strip()
except:
date_updated = "Not updated"
+ try:
+ description = soup.find('div', class_='workshopItemDescription').get_text(separator='\n')
+ except:
+ description = "Not available"
+
stars_div = soup.find("div", class_="fileRatingDetails")
starts = stars_div.find("img")["src"]
except:
@@ -664,7 +635,7 @@ class BOIIIWD(ctk.CTk):
image_size = image.size
self.toplevel_info_window(map_name, map_mod_type, map_size, image, image_size, date_created ,
- date_updated, stars_image, stars_image_size, ratings_text, url)
+ date_updated, stars_image, stars_image_size, ratings_text, url, workshop_id, description)
except requests.exceptions.RequestException as e:
show_message("Error", f"Failed to fetch map information.\nError: {e}", icon="cancel")
@@ -674,7 +645,8 @@ class BOIIIWD(ctk.CTk):
info_thread.start()
def toplevel_info_window(self, map_name, map_mod_type, map_size, image, image_size,
- date_created ,date_updated, stars_image, stars_image_size, ratings_text, url):
+ date_created ,date_updated, stars_image, stars_image_size,
+ ratings_text, url, workshop_id, description):
def main_thread():
top = ctk.CTkToplevel(self)
top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")))
@@ -687,6 +659,34 @@ class BOIIIWD(ctk.CTk):
def view_map_mod():
webbrowser.open(url)
+ def show_description(event):
+ def main_thread():
+ description_window = ctk.CTkToplevel(None)
+
+ if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")):
+ description_window.after(210, lambda: description_window.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")))
+
+ description_window.attributes('-topmost', 'true')
+ description_window.title(f"Description - {map_name}")
+ x_pos = event.x_root - 300
+ y_pos = event.y_root - 200
+ calc_req_width = len(description) * 6 + 5
+ win_width = calc_req_width if calc_req_width < 500 else 500
+ description_window.geometry(f"{win_width + 5}x300+{x_pos}+{y_pos}")
+
+ if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_obsidian.json":
+ description_label = ctk.CTkTextbox(description_window, activate_scrollbars=True, scrollbar_button_color="#5b6c7f")
+ else:
+ description_label = ctk.CTkTextbox(description_window, activate_scrollbars=True)
+ description_label.insert("1.0", description)
+ description_label.pack(fill=ctk.BOTH, expand=True, padx=(10, 10), pady=(10, 10))
+ description_label.configure(state="disabled")
+
+ self.create_context_menu(description_label, textbox=True)
+ description_window.after(50, description_window.focus_set)
+
+ self.after(0, main_thread)
+
# frames
stars_frame = ctk.CTkFrame(top)
stars_frame.grid(row=0, column=0, columnspan=2, padx=20, pady=(20, 0), sticky="nsew")
@@ -706,17 +706,37 @@ class BOIIIWD(ctk.CTk):
name_label = ctk.CTkLabel(info_frame, text=f"Name: {map_name}")
name_label.grid(row=0, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ desc_threshold = 30
+ shortened_description = re.sub(r'\n', '', description).strip()
+ shortened_description = re.sub(r'([^a-zA-Z0-9\s:().])', '', shortened_description)
+ shortened_description = f"{shortened_description[:desc_threshold]}... (View)"\
+ if len(shortened_description) > desc_threshold else shortened_description
+ description_lab = ctk.CTkLabel(info_frame, text=f"Description: {shortened_description}")
+ description_lab.grid(row=1, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ if len(description) > desc_threshold:
+ description_lab_tooltip = CTkToolTip(description_lab, message="View description", topmost=True)
+ description_lab.configure(cursor="hand2")
+ description_lab.bind("", lambda e: show_description(e))
+
type_label = ctk.CTkLabel(info_frame, text=f"Type: {map_mod_type}")
- type_label.grid(row=1, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ type_label.grid(row=2, column=0, columnspan=2, sticky="w", padx=20, pady=5)
size_label = ctk.CTkLabel(info_frame, text=f"Size: {map_size}")
- size_label.grid(row=2, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ size_label.grid(row=3, column=0, columnspan=2, sticky="w", padx=20, pady=5)
date_created_label = ctk.CTkLabel(info_frame, text=f"Posted: {date_created}")
- date_created_label.grid(row=3, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ date_created_label.grid(row=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
- date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated}")
- date_updated_label.grid(row=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
+ if date_updated != "Not updated":
+ date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated} 🔗")
+ date_updated_label_tooltip = CTkToolTip(date_updated_label, message="View changelogs", topmost=True)
+ date_updated_label.configure(cursor="hand2")
+ date_updated_label.bind("", lambda e:
+ webbrowser.open(f"https://steamcommunity.com/sharedfiles/filedetails/changelog/{workshop_id}"))
+ else:
+ date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated}")
+
+ date_updated_label.grid(row=5, column=0, columnspan=2, sticky="w", padx=20, pady=5)
stars_image_label = ctk.CTkLabel(stars_frame)
stars_width, stars_height = stars_image_size
@@ -804,14 +824,16 @@ class BOIIIWD(ctk.CTk):
# the real deal
def run_steamcmd_command(self, command, map_folder, wsid, queue=None):
steamcmd_path = get_steamcmd_path()
- stdout = os.path.join(steamcmd_path, "logs", "workshop_log.txt")
- timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
+ stdout_path = os.path.join(steamcmd_path, "logs", "workshop_log.txt")
+ timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
+
+ os.makedirs(os.path.dirname(stdout_path), exist_ok=True)
try:
- with open(stdout, 'w') as file:
+ with open(stdout_path, 'w') as file:
file.write('')
except:
- os.rename(stdout, os.path.join(map_folder, os.path.join(stdout, f"workshop_log_couldntremove_{timestamp}.txt")))
+ os.rename(stdout_path, os.path.join(map_folder, os.path.join(stdout_path, f"workshop_log_couldntremove_{timestamp}.txt")))
show_console = subprocess.CREATE_NO_WINDOW
if self.settings_tab.console:
@@ -846,7 +868,7 @@ class BOIIIWD(ctk.CTk):
#wait for process
while True:
if not self.is_downloading:
- if self.check_steamcmd_stdout(stdout, wsid):
+ if self.check_steamcmd_stdout(stdout_path, wsid):
start_time = time.time()
self.is_downloading = True
elapsed_time = time.time() - start_time
@@ -857,10 +879,10 @@ class BOIIIWD(ctk.CTk):
# print("Broken freeeee!")
self.is_downloading = False
try:
- with open(stdout, 'w') as file:
+ with open(stdout_path, 'w') as file:
file.write('')
except:
- os.rename(stdout, os.path.join(map_folder, os.path.join(stdout, f"workshop_log_couldntremove_{timestamp}.txt")))
+ os.rename(stdout_path, os.path.join(map_folder, os.path.join(stdout_path, f"workshop_log_couldntremove_{timestamp}.txt")))
if not self.settings_tab.stopped:
self.settings_tab.steam_fail_counter = self.settings_tab.steam_fail_counter + 1
@@ -892,7 +914,7 @@ class BOIIIWD(ctk.CTk):
while True:
if not self.is_downloading:
- if self.check_steamcmd_stdout(stdout, wsid):
+ if self.check_steamcmd_stdout(stdout_path, wsid):
self.is_downloading = True
if process.poll() != None:
break
@@ -901,10 +923,10 @@ class BOIIIWD(ctk.CTk):
# print("Broken freeeee!")
self.is_downloading = False
try:
- with open(stdout, 'w') as file:
+ with open(stdout_path, 'w') as file:
file.write('')
except:
- os.rename(stdout, os.path.join(map_folder, os.path.join(stdout, f"workshop_log_couldntremove_{timestamp}.txt")))
+ os.rename(stdout_path, os.path.join(map_folder, os.path.join(stdout_path, f"workshop_log_couldntremove_{timestamp}.txt")))
if not os.path.exists(map_folder):
show_message("SteamCMD has terminated", "SteamCMD has been terminated\nAnd failed to download the map/mod, try again or enable continuous download in settings")
@@ -939,7 +961,8 @@ class BOIIIWD(ctk.CTk):
return
self.after(0, callback)
- def download_map(self):
+ @if_internet_available
+ def download_map(self, update=False):
self.is_downloading = False
self.fail_threshold = 0
if not self.is_pressed:
@@ -948,15 +971,15 @@ class BOIIIWD(ctk.CTk):
self.library_tab.load_items(self.edit_destination_folder.get())
if self.queue_enabled:
self.item_skipped = False
- start_down_thread = threading.Thread(target=self.queue_download_thread)
+ start_down_thread = threading.Thread(target=self.queue_download_thread, args=(update,))
start_down_thread.start()
else:
- start_down_thread = threading.Thread(target=self.download_thread)
+ start_down_thread = threading.Thread(target=self.download_thread, args=(update,))
start_down_thread.start()
else:
show_message("Warning", "Already pressed, Please wait.")
- def queue_download_thread(self):
+ def queue_download_thread(self, update=None):
self.stopped = False
self.queue_stop_button = False
try:
@@ -1032,18 +1055,19 @@ class BOIIIWD(ctk.CTk):
if any(workshop_id in item for item in self.library_tab.added_items):
self.already_installed.append(workshop_id)
- if self.already_installed:
- item_ids = ", ".join(self.already_installed)
- if self.settings_tab.skip_already_installed:
- for item in self.already_installed:
- if item in items:
- items.remove(item)
- show_message("Heads up!, map/s skipped => skip is on in settings", f"These item IDs may already be installed and are skipped:\n{item_ids}", icon="info")
- if not any(isinstance(item, int) for item in items):
- self.stop_download()
- return
- else:
- show_message("Heads up! map/s not skipped => skip is off in settings", f"These item IDs may already be installed:\n{item_ids}", icon="info")
+ if not update:
+ if self.already_installed:
+ item_ids = ", ".join(self.already_installed)
+ if self.settings_tab.skip_already_installed:
+ for item in self.already_installed:
+ if item in items:
+ items.remove(item)
+ show_message("Heads up!, map/s skipped => skip is on in settings", f"These item IDs may already be installed and are skipped:\n{item_ids}", icon="info")
+ if not any(isinstance(item, int) for item in items):
+ self.stop_download()
+ return
+ else:
+ show_message("Heads up! map/s not skipped => skip is off in settings", f"These item IDs may already be installed:\n{item_ids}", icon="info")
self.after(1, self.status_text.configure(text=f"Status: Total size: ~{convert_bytes_to_readable(self.total_queue_size)}"))
start_time = time.time()
@@ -1210,6 +1234,7 @@ class BOIIIWD(ctk.CTk):
update_ui_thread.daemon = True
update_ui_thread.start()
update_ui_thread.join()
+
self.progress_text.configure(text="0%")
self.progress_bar.set(0.0)
@@ -1220,18 +1245,39 @@ class BOIIIWD(ctk.CTk):
if os.path.exists(json_file_path):
self.label_speed.configure(text="Installing...")
mod_type = extract_json_data(json_file_path, "Type")
- folder_name = extract_json_data(json_file_path, "FolderName")
+ items_file = os.path.join(application_path, LIBRARY_FILE)
+ item_exixsts = self.library_tab.item_exists_in_file(items_file, workshop_id)
+
+ if item_exixsts:
+ get_folder_name = self.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name")
+ if get_folder_name:
+ folder_name = get_folder_name
+ else:
+ try:
+ folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
+ except:
+ folder_name = extract_json_data(json_file_path, "publisherID")
+ else:
+ try:
+ folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
+ except:
+ folder_name = extract_json_data(json_file_path, "publisherID")
if mod_type == "mod":
- mods_folder = os.path.join(destination_folder, "mods")
- folder_name_path = os.path.join(mods_folder, folder_name, "zone")
+ path_folder = os.path.join(destination_folder, "mods")
+ folder_name_path = os.path.join(path_folder, folder_name, "zone")
elif mod_type == "map":
- usermaps_folder = os.path.join(destination_folder, "usermaps")
- folder_name_path = os.path.join(usermaps_folder, folder_name, "zone")
+ path_folder = os.path.join(destination_folder, "usermaps")
+ folder_name_path = os.path.join(path_folder, folder_name, "zone")
else:
show_message("Error", f"Invalid workshop type in workshop.json, are you sure this is a map or a mod?., skipping {workshop_id}...", icon="cancel")
return
+ if not item_exixsts:
+ while os.path.exists(os.path.join(path_folder, folder_name)):
+ folder_name += f"_{workshop_id}"
+ folder_name_path = os.path.join(path_folder, folder_name, "zone")
+
os.makedirs(folder_name_path, exist_ok=True)
try:
@@ -1243,6 +1289,8 @@ class BOIIIWD(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)
+
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)"))
self.show_complete_message(message=f"All files were downloaded\nYou can run the game now!\nPS: You have to restart the game \n(pressing launch will launch/restarts)")
@@ -1275,7 +1323,7 @@ class BOIIIWD(ctk.CTk):
self.stop_download()
self.is_pressed = False
- def download_thread(self):
+ def download_thread(self, update=None):
try:
self.settings_tab.stopped = False
@@ -1332,12 +1380,13 @@ class BOIIIWD(ctk.CTk):
self.stop_download()
return
- if any(workshop_id in item for item in self.library_tab.added_items):
- if self.settings_tab.skip_already_installed:
- show_message("Heads up!, map skipped => Skip is on in settings", f"This item may already be installed, Stopping: {workshop_id}", icon="info")
- self.stop_download()
- return
- show_message("Heads up! map not skipped => Skip is off in settings", f"This item may already be installed: {workshop_id}", icon="info")
+ if not update:
+ if any(workshop_id in item for item in self.library_tab.added_items):
+ if self.settings_tab.skip_already_installed:
+ show_message("Heads up!, map skipped => Skip is on in settings", f"This item may already be installed, Stopping: {workshop_id}", icon="info")
+ self.stop_download()
+ return
+ show_message("Heads up! map not skipped => Skip is off in settings", f"This item may already be installed: {workshop_id}", icon="info")
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)
@@ -1462,19 +1511,40 @@ class BOIIIWD(ctk.CTk):
if os.path.exists(json_file_path):
self.label_speed.configure(text="Installing...")
mod_type = extract_json_data(json_file_path, "Type")
- folder_name = extract_json_data(json_file_path, "FolderName")
+ items_file = os.path.join(application_path, LIBRARY_FILE)
+ item_exixsts = self.library_tab.item_exists_in_file(items_file, workshop_id)
+
+ if item_exixsts:
+ get_folder_name = self.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name")
+ if get_folder_name:
+ folder_name = get_folder_name
+ else:
+ try:
+ folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
+ except:
+ folder_name = extract_json_data(json_file_path, "publisherID")
+ else:
+ try:
+ folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
+ except:
+ folder_name = extract_json_data(json_file_path, "publisherID")
if mod_type == "mod":
- mods_folder = os.path.join(destination_folder, "mods")
- folder_name_path = os.path.join(mods_folder, folder_name, "zone")
+ path_folder = os.path.join(destination_folder, "mods")
+ folder_name_path = os.path.join(path_folder, folder_name, "zone")
elif mod_type == "map":
- usermaps_folder = os.path.join(destination_folder, "usermaps")
- folder_name_path = os.path.join(usermaps_folder, folder_name, "zone")
+ path_folder = os.path.join(destination_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")
self.stop_download()
return
+ if not item_exixsts:
+ while os.path.exists(os.path.join(path_folder, folder_name)):
+ folder_name += f"_{workshop_id}"
+ folder_name_path = os.path.join(path_folder, folder_name, "zone")
+
os.makedirs(folder_name_path, exist_ok=True)
try:
@@ -1486,6 +1556,7 @@ class BOIIIWD(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.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")
diff --git a/boiiiwd_package/src/resources/ryuk.ico b/boiiiwd_package/src/resources/ryuk.ico
deleted file mode 100644
index 981a640..0000000
Binary files a/boiiiwd_package/src/resources/ryuk.ico and /dev/null differ
diff --git a/boiiiwd_package/src/resources/sett4.png b/boiiiwd_package/src/resources/sett4.png
deleted file mode 100644
index 94f5299..0000000
Binary files a/boiiiwd_package/src/resources/sett4.png and /dev/null differ
diff --git a/boiiiwd_package/src/resources/sett5.png b/boiiiwd_package/src/resources/sett5.png
deleted file mode 100644
index d22bfc3..0000000
Binary files a/boiiiwd_package/src/resources/sett5.png and /dev/null differ
diff --git a/boiiiwd_package/src/resources/sett6.png b/boiiiwd_package/src/resources/sett6.png
deleted file mode 100644
index 79b4cbf..0000000
Binary files a/boiiiwd_package/src/resources/sett6.png and /dev/null differ
diff --git a/boiiiwd_package/src/resources/sett7.png b/boiiiwd_package/src/resources/sett7.png
deleted file mode 100644
index c232b81..0000000
Binary files a/boiiiwd_package/src/resources/sett7.png and /dev/null differ
diff --git a/boiiiwd_package/src/resources/sett8.png b/boiiiwd_package/src/resources/sett8.png
deleted file mode 100644
index fcf256d..0000000
Binary files a/boiiiwd_package/src/resources/sett8.png and /dev/null differ
diff --git a/boiiiwd_package/src/settings_tab.py b/boiiiwd_package/src/settings_tab.py
index 8c4edb8..ae3e435 100644
--- a/boiiiwd_package/src/settings_tab.py
+++ b/boiiiwd_package/src/settings_tab.py
@@ -1,46 +1,9 @@
+from src.update_window import check_for_updates_func
from src.imports import *
-from src.helpers import show_message, cwd, check_config,\
- save_config, reset_steamcmd, launch_boiii_func, get_latest_release_version
+from src.helpers import *
-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(".", ""))
+import src.shared_vars as main_app
- 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)
-
- result = msg_box.get()
-
- if result == "View":
- webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest")
-
- from src.update_window import UpdateWindow
- if result == "Yes":
- update_window = UpdateWindow(window, LATEST_RELEASE_URL)
- update_window.start_update()
-
- if result == "No":
- return
-
- elif int_latest_version < int_current_version:
- if ignore_up_todate:
- return
- msg_box = CTkMessagebox(title="Up to Date!", message=f"Unreleased version!\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="Ok", sound=True)
- result = msg_box.get()
- 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)
- result = msg_box.get()
-
- else:
- 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")
class SettingsTab(ctk.CTkFrame):
def __init__(self, master=None):
@@ -57,6 +20,7 @@ class SettingsTab(ctk.CTkFrame):
self.steam_fail_number = 10
self.steamcmd_reset = False
self.show_fails = True
+ self.check_items_on_launch = False
# Left and right frames, use fg_color="transparent"
self.grid_rowconfigure(0, weight=1)
@@ -113,7 +77,7 @@ class SettingsTab(ctk.CTkFrame):
# Show 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 (on top of progress bar)", 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"))
@@ -121,21 +85,39 @@ class SettingsTab(ctk.CTkFrame):
# Show skip_already_installed maps checkbox
self.skip_already_installed_var = ctk.BooleanVar()
self.skip_already_installed_var.trace_add("write", self.enable_save_button)
- self.skip_already_installed_ch = ctk.CTkSwitch(left_frame, text="Skip already installed maps:", variable=self.skip_already_installed_var)
+ self.skip_already_installed_ch = ctk.CTkSwitch(left_frame, text="Skip already installed maps", variable=self.skip_already_installed_var)
self.skip_already_installed_ch.grid(row=6, column=1, padx=20, pady=(20, 0), sticky="nw")
self.skip_already_installed_ch_tooltip = CTkToolTip(self.skip_already_installed_ch, message="If on it will not download installed maps,\nthis can miss sometimes if you remove maps manually and not from library tab while the app is running")
self.skip_already_installed_var.set(self.load_settings("skip_already_installed", "on"))
+ # 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.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"))
+
# Resetr 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 on % fails: (n of fails)", anchor="w")
- self.reset_steamcmd_on_fail_text.grid(row=7, column=1, padx=20, pady=(10, 0), sticky="nw")
+ 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=20, pady=(0, 0), sticky="nw")
+ 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.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,
+ 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.set(value=self.load_settings("folder_naming", "PublisherID"))
+
# Check for updates button n Launch boiii
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")
@@ -145,28 +127,32 @@ class SettingsTab(ctk.CTkFrame):
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 or not so use at ur own risk (could fix some issues as well)")
+ 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_boiii = ctk.CTkButton(right_frame, text="Steam to boiii", command=self.from_steam_to_boiii_toplevel)
+ self.steam_to_boiii.grid(row=5, column=1, padx=20, pady=(20, 0), sticky="n")
+ self.steam_to_boiii_tooltip = CTkToolTip(self.steam_to_boiii, message="Moves/copies maps and mods from steam to boiii (opens up a window)")
# appearance
self.appearance_mode_label = ctk.CTkLabel(right_frame, text="Appearance Mode:", anchor="n")
- self.appearance_mode_label.grid(row=4, column=1, padx=20, pady=(20, 0))
+ 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=5, column=1, padx=20, pady=(0, 0))
+ 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=6, column=1, padx=20, pady=(10, 0))
+ 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=7, column=1, padx=20, pady=(0, 0))
+ 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.boiiiwd_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=8, column=1, padx=20, pady=(10, 0))
+ 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=9, column=1, padx=20, pady=(0, 0))
+ 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
@@ -187,7 +173,7 @@ class SettingsTab(ctk.CTkFrame):
if response == "No":
return
elif response == "Ok":
- os.system(f"notepad {os.path.join(cwd(), 'config.ini')}")
+ os.system(f"notepad {os.path.join(application_path, 'config.ini')}")
else:
return
self.after(0, callback)
@@ -195,6 +181,7 @@ class SettingsTab(ctk.CTkFrame):
show_message("Couldn't open config.ini" ,"you can do so by yourself and change reset_on_fail value to whatever you want")
else:
return
+
def theme_options_func(self, option: str):
if option == "Default":
self.boiiiwd_custom_theme(disable_only=True)
@@ -231,13 +218,22 @@ class SettingsTab(ctk.CTkFrame):
pass
def enable_save_button(self, *args):
- try:
- self.save_button.configure(state='normal')
- except:
- pass
+ try: self.save_button.configure(state='normal')
+ except: pass
def save_settings(self):
self.save_button.configure(state='disabled')
+
+ if self.folder_options.get() == "PublisherID":
+ save_config("folder_naming", "0")
+ else:
+ save_config("folder_naming", "1")
+
+ if self.check_items_var.get():
+ save_config("check_items", "on")
+ else:
+ save_config("check_items", "off")
+
if self.check_updates_checkbox.get():
save_config("checkforupdtes", "on")
else:
@@ -295,6 +291,12 @@ class SettingsTab(ctk.CTkFrame):
save_config("reset_on_fail", value)
def load_settings(self, setting, fallback=None):
+ if setting == "folder_naming":
+ if check_config(setting, fallback) == "1":
+ return "FolderName"
+ else:
+ return "PublisherID"
+
if setting == "console":
if check_config(setting, fallback) == "on":
self.console = True
@@ -358,7 +360,7 @@ class SettingsTab(ctk.CTkFrame):
return 0
if setting == "theme":
- if os.path.exists(os.path.join(cwd(), "boiiiwd_theme.json")):
+ if os.path.exists(os.path.join(application_path, "boiiiwd_theme.json")):
return "Custom"
if check_config("theme", "boiiiwd_theme.json") == "boiiiwd_theme.json":
return "Default"
@@ -379,11 +381,11 @@ class SettingsTab(ctk.CTkFrame):
return 0
def boiiiwd_custom_theme(self, disable_only=None):
- file_to_rename = os.path.join(cwd(), "boiiiwd_theme.json")
+ file_to_rename = os.path.join(application_path, "boiiiwd_theme.json")
if os.path.exists(file_to_rename):
- timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
+ timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
new_name = f"boiiiwd_theme_{timestamp}.json"
- os.rename(file_to_rename, os.path.join(cwd(), new_name))
+ os.rename(file_to_rename, os.path.join(application_path, new_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")
@@ -391,14 +393,71 @@ class SettingsTab(ctk.CTkFrame):
if disable_only:
return
try:
- shutil.copy(os.path.join(RESOURCES_DIR, check_config("theme", "boiiiwd_theme.json")), os.path.join(cwd(), "boiiiwd_theme.json"))
+ shutil.copy(os.path.join(RESOURCES_DIR, check_config("theme", "boiiiwd_theme.json")), os.path.join(application_path, "boiiiwd_theme.json"))
except:
- shutil.copy(os.path.join(RESOURCES_DIR, "boiiiwd_theme.json"), os.path.join(cwd(), "boiiiwd_theme.json"))
+ shutil.copy(os.path.join(RESOURCES_DIR, "boiiiwd_theme.json"), os.path.join(application_path, "boiiiwd_theme.json"))
show_message("Preset file created", "You can now edit boiiiwd_theme.json in the current directory to your liking\n* Edits will apply next time you open boiiiwd\n* Program will always take boiiiwd_theme.json as the first theme option if found\n* Click on this button again to disable your custom theme or just rename boiiiwd_theme.json", icon="info")
def settings_check_for_updates(self):
check_for_updates_func(self, ignore_up_todate=False)
+ def rename_all_folders(self, option):
+ boiiiFolder = main_app.app.edit_destination_folder.get()
+ maps_folder = os.path.join(boiiiFolder, "mods")
+ mods_folder = os.path.join(boiiiFolder, "usermaps")
+ folders_to_process = [mods_folder, maps_folder]
+ 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
+
+ json_path = os.path.join(zone_path, "workshop.json")
+ new_name = extract_json_data(json_path, option)
+ if folder_name == new_name:
+ continue
+
+ if os.path.exists(json_path):
+ publisher_id = extract_json_data(json_path, 'PublisherID')
+ folder_to_rename = os.path.join(folder_path, folder_name)
+ new_folder_name = new_name
+ while new_folder_name in processed_names:
+ new_folder_name += f"_{publisher_id}"
+ new_path = os.path.join(folder_path, new_folder_name)
+
+ while os.path.exists(new_path):
+ new_folder_name += f"_{publisher_id}"
+ new_path = os.path.join(folder_path, new_folder_name)
+
+ os.rename(folder_to_rename, new_path)
+ processed_names.add(new_folder_name)
+
+ def change_folder_naming(self, option):
+ main_app.app.title("BOIII 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 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("BOIII Workshop Downloader - Settings ➜ Renaming... ⏳")
+ try :self.rename_all_folders(option)
+ except Exception as er: show_message("Error!", f"Error occured when renaming\n{er}"); return
+ show_message("Done!", "All folders have been renamed", icon="info")
+ else:
+ show_message("Heads up!", "Only newly downloaded items will be affected", icon="info")
+ else:
+ show_message("Warning -> Check boiii 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 boiii 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:
+ main_app.app.title("BOIII Workshop Downloader - Settings")
+ self.save_settings()
+
def load_on_switch_screen(self):
self.check_updates_var.set(self.load_settings("checkforupdtes"))
self.console_var.set(self.load_settings("console"))
@@ -417,3 +476,215 @@ class SettingsTab(ctk.CTkFrame):
def settings_reset_steamcmd(self):
reset_steamcmd()
+
+ def from_steam_to_boiii_toplevel(self):
+ def main_thread():
+ 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)
+ 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 boiii -> Workshop items")
+ top.attributes('-topmost', 'true')
+ top.resizable(False, False)
+ # Create input boxes
+ center_frame = ctk.CTkFrame(top)
+ center_frame.grid(row=0, column=0, padx=20, pady=20)
+
+ # Create input boxes
+ steam_folder_label = ctk.CTkLabel(center_frame, text="Steam Folder:")
+ steam_folder_label.grid(row=0, column=0, padx=(20, 20), pady=(10, 0), sticky='w')
+ steam_folder_entry = ctk.CTkEntry(center_frame, width=225)
+ steam_folder_entry.grid(row=1, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes')
+ button_steam_browse = ctk.CTkButton(center_frame, text="Select", width=10)
+ button_steam_browse.grid(row=1, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes")
+
+ boiii_folder_label = ctk.CTkLabel(center_frame, text="boiii Folder:")
+ boiii_folder_label.grid(row=2, column=0, padx=(20, 20), pady=(10, 0), sticky='w')
+ boiii_folder_entry = ctk.CTkEntry(center_frame, width=225)
+ boiii_folder_entry.grid(row=3, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes')
+ button_BOIII_browse = ctk.CTkButton(center_frame, text="Select", width=10)
+ button_BOIII_browse.grid(row=3, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes")
+
+ # Create option to choose between cut or copy
+ operation_label = ctk.CTkLabel(center_frame, text="Choose operation:")
+ operation_label.grid(row=4, column=0, padx=(20, 20), pady=(10, 10), sticky='wnes')
+ copy_var = ctk.BooleanVar()
+ cut_var = ctk.BooleanVar()
+ copy_check = ctk.CTkCheckBox(center_frame, text="Copy", variable=copy_var)
+ cut_check = ctk.CTkCheckBox(center_frame, text="Cut", variable=cut_var)
+ copy_check.grid(row=4, column=1, padx=(0, 10), pady=(10, 10), sticky='wnes')
+ cut_check.grid(row=4, column=2, padx=(0, 10), pady=(10, 10), sticky='nes')
+
+ # Create progress bar
+ progress_bar = ctk.CTkProgressBar(center_frame, mode="determinate", height=20, corner_radius=7)
+ progress_bar.grid(row=5, column=0, columnspan=3, padx=(20, 20), pady=(10, 10), sticky='wnes')
+ progress_text = ctk.CTkLabel(progress_bar, text="0%", font=("Helvetica", 12), fg_color="transparent", text_color="white", height=0, width=0, corner_radius=0)
+ progress_text.place(relx=0.5, rely=0.5, anchor="center")
+
+ copy_button = ctk.CTkButton(center_frame, text="Start (Copy)")
+ copy_button.grid(row=6, column=0, columnspan=3,padx=(20, 20), pady=(10, 10), sticky='wnes')
+
+ # funcs
+ # had to use this shit again cuz of threading issues with widgets
+ def copy_with_progress(src, dst):
+ try:
+ total_files = sum([len(files) for root, dirs, files in os.walk(src)])
+ progress = 0
+
+ def copy_progress(src, dst):
+ nonlocal progress
+ shutil.copy2(src, dst)
+ progress += 1
+ top.after(0, progress_text.configure(text=f"Copying files: {progress}/{total_files}"))
+ value = (progress / total_files) * 100
+ valuep = value / 100
+ progress_bar.set(valuep)
+
+ try:
+ shutil.copytree(src, dst, dirs_exist_ok=True, copy_function=copy_progress)
+ except Exception as E:
+ show_message("Error", f"Error copying files: {E}", icon="cancel")
+ finally:
+ top.after(0, progress_text.configure(text="0%"))
+ top.after(0, progress_bar.set(0.0))
+
+ def check_status(var, op_var):
+ if var.get():
+ op_var.set(False)
+ if cut_var.get():
+ copy_button.configure(text=f"Start (Cut)")
+ if copy_var.get():
+ copy_button.configure(text=f"Start (Copy)")
+
+ def open_BOIII_browser():
+ selected_folder = ctk.filedialog.askdirectory(title="Select boiii Folder")
+ if selected_folder:
+ boiii_folder_entry.delete(0, "end")
+ boiii_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)")
+ if selected_folder:
+ steam_folder_entry.delete(0, "end")
+ steam_folder_entry.insert(0, selected_folder)
+ save_config("steam_folder" ,steam_folder_entry.get())
+
+ def start_copy_operation():
+ 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!")
+ return
+
+ copy_button.configure(state="disabled")
+ steam_folder = steam_folder_entry.get()
+ ws_folder = os.path.join(steam_folder, "steamapps/workshop/content/311210")
+ boiii_folder = boiii_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(boiii_folder):
+ show_message("Not found", "boiii folder not found, please recheck path")
+ return
+
+ top.after(0, progress_text.configure(text="Loading..."))
+
+ map_folder = os.path.join(ws_folder)
+
+ subfolders = [f for f in os.listdir(map_folder) if os.path.isdir(os.path.join(map_folder, f))]
+ total_folders = len(subfolders)
+
+ if not subfolders:
+ show_message("No items found", f"No items found in \n{map_folder}")
+ return
+
+ for i, workshop_id in enumerate(subfolders, start=1):
+ json_file_path = os.path.join(map_folder, workshop_id, "workshop.json")
+ copy_button.configure(text=f"Working on -> {i}/{total_folders}")
+
+ if os.path.exists(json_file_path):
+ mod_type = extract_json_data(json_file_path, "Type")
+ items_file = os.path.join(application_path, LIBRARY_FILE)
+ item_exixsts = main_app.app.library_tab.item_exists_in_file(items_file, workshop_id)
+
+ if item_exixsts:
+ get_folder_name = main_app.app.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name")
+ if get_folder_name:
+ folder_name = get_folder_name
+ else:
+ try:
+ folder_name = extract_json_data(json_file_path, main_app.app.settings_tab.folder_options.get())
+ except:
+ folder_name = extract_json_data(json_file_path, "publisherID")
+ else:
+ try:
+ folder_name = extract_json_data(json_file_path, main_app.app.settings_tab.folder_options.get())
+ except:
+ folder_name = extract_json_data(json_file_path, "publisherID")
+
+ if mod_type == "mod":
+ path_folder = os.path.join(boiii_folder, "mods")
+ folder_name_path = os.path.join(path_folder, folder_name, "zone")
+ elif mod_type == "map":
+ path_folder = os.path.join(boiii_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")
+ continue
+
+ if not item_exixsts:
+ while os.path.exists(os.path.join(path_folder, folder_name)):
+ folder_name += f"_{workshop_id}"
+ folder_name_path = os.path.join(path_folder, folder_name, "zone")
+
+ os.makedirs(folder_name_path, exist_ok=True)
+
+ try:
+ copy_with_progress(os.path.join(map_folder, workshop_id), folder_name_path)
+ except Exception as E:
+ show_message("Error", f"Error copying files: {E}", icon="cancel")
+ continue
+
+ if cut_var.get():
+ remove_tree(os.path.join(map_folder, workshop_id))
+
+ main_app.app.library_tab.update_item(main_app.app.edit_destination_folder.get(), workshop_id, mod_type, folder_name)
+
+ if subfolders:
+ 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:
+ if cut_var.get():
+ copy_button.configure(text=f"Start (Cut)")
+ if copy_var.get():
+ copy_button.configure(text=f"Start (Copy)")
+ copy_button.configure(state="normal")
+ top.after(0, progress_bar.set(0))
+ top.after(0, progress_text.configure(text="0%"))
+
+ # prevents app hanging
+ threading.Thread(target=start_thread).start()
+
+ # config
+ progress_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "progress_bar_fill_color")
+ progress_bar.configure(progress_color=progress_color)
+ steam_folder_entry.insert(1, check_config("steam_folder", ""))
+ boiii_folder_entry.insert(1, main_app.app.edit_destination_folder.get())
+ button_BOIII_browse.configure(command=open_BOIII_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(boiii_folder_entry)
+ copy_var.set(True)
+ progress_bar.set(0)
+
+ except Exception as e:
+ show_message("Error", f"{e}", icon="cancel")
+
+ main_app.app.after(0, main_thread)
diff --git a/boiiiwd_package/src/shared_vars.py b/boiiiwd_package/src/shared_vars.py
new file mode 100644
index 0000000..af5b483
--- /dev/null
+++ b/boiiiwd_package/src/shared_vars.py
@@ -0,0 +1,3 @@
+import src.main as main
+
+app = main.BOIIIWD()
diff --git a/boiiiwd_package/src/update_window.py b/boiiiwd_package/src/update_window.py
index 069128d..a70af6c 100644
--- a/boiiiwd_package/src/update_window.py
+++ b/boiiiwd_package/src/update_window.py
@@ -1,11 +1,49 @@
+import src.shared_vars as main_app
from src.imports import *
-from src.helpers import show_message, check_config, check_custom_theme,\
- get_button_state_colors, convert_bytes_to_readable, create_update_script
+from src.helpers import *
+
+
+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(".", ""))
+
+ 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)
+
+ result = msg_box.get()
+
+ if result == "View":
+ webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest")
+
+ if result == "Yes":
+ update_window = UpdateWindow(window, LATEST_RELEASE_URL)
+ update_window.start_update()
+
+ if result == "No":
+ return
+
+ elif int_latest_version < int_current_version:
+ if ignore_up_todate:
+ return
+ msg_box = CTkMessagebox(title="Up to Date!", message=f"Unreleased version!\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="Ok", sound=True)
+ result = msg_box.get()
+ 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)
+ result = msg_box.get()
+
+ else:
+ 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")
class UpdateWindow(ctk.CTkToplevel):
def __init__(self, master, update_url):
- global master_win
- master_win = master
super().__init__(master)
self.title("BOIIIWD Self-Updater")
self.geometry("400x150")
@@ -42,7 +80,7 @@ class UpdateWindow(ctk.CTkToplevel):
def update_progress_bar(self):
try:
- update_dir = os.path.join(os.getcwd(), UPDATER_FOLDER)
+ update_dir = os.path.join(application_path, UPDATER_FOLDER)
response = requests.get(LATEST_RELEASE_URL, stream=True)
response.raise_for_status()
current_exe = sys.argv[0]
@@ -95,13 +133,11 @@ class UpdateWindow(ctk.CTkToplevel):
os.remove(fr"{zip_path}")
self.label_download.configure(text="Update cancelled.")
self.progress_bar.set(0.0)
- # there's a better solution ill implement it later
- global master_win
- try:
- master_win.attributes('-alpha', 1.0)
- except:
- pass
+
+ try: main_app.app.attributes('-alpha', 1.0)
+ except: pass
show_message("Cancelled!", "Update cancelled by user", icon="warning")
+
except Exception as e:
self.progress_bar.set(0.0)
self.label_download.configure(text="Update failed")
diff --git a/build.py b/build.py
index 8032945..101f272 100644
--- a/build.py
+++ b/build.py
@@ -4,8 +4,8 @@ from distutils.sysconfig import get_python_lib
site_packages_path = get_python_lib()
NAME = "BOIIIWD"
-SCRIPT = "boiiiwd.py"
-ICON = "boiiiwd_package/ryuk.ico"
+SCRIPT = "boiiiwd_package/boiiiwd.py"
+ICON = "boiiiwd_package/resources/ryuk.ico"
PyInstaller.__main__.run([
"{}".format(SCRIPT),
@@ -15,7 +15,14 @@ PyInstaller.__main__.run([
"--windowed",
"--ascii",
"--icon", f"{ICON}",
- "--add-data", "boiiiwd_package/src/resources;resources",
+ "--add-data", "boiiiwd_package/resources;resources",
+ "--add-data", "boiiiwd_package/src;imports",
+ "--add-data", "boiiiwd_package/src;helpers",
+ "--add-data", "boiiiwd_package/src;shared_vars",
+ "--add-data", "boiiiwd_package/src;library_tab",
+ "--add-data", "boiiiwd_package/src;settings_tab",
+ "--add-data", "boiiiwd_package/src;update_window",
+ "--add-data", "boiiiwd_package/src;main",
"--add-data", f"{site_packages_path}\customtkinter;customtkinter",
"--add-data", f"{site_packages_path}\CTkMessagebox;CTkMessagebox",
"--add-data", f"{site_packages_path}\CTkToolTip;CTkToolTip",