Merge pull request #3 from faroukbmiled/refactored

Project modularization: Transitioning to a package-based app
This commit is contained in:
Ryuk 2023-09-23 08:16:10 -07:00 committed by GitHub
commit 0eac002347
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1437 additions and 4814 deletions

2
.gitignore vendored
View File

@ -161,4 +161,6 @@ cython_debug/
# Other
*.ini
*.conf
dist/
.vscode

View File

@ -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 @@
<a name="freezing"></a>
## 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:\<python_path>\lib\site-packages\customtkinter;customtkinter\" --add-data "c:\<python_path>\lib\site-packages\CTkMessagebox;CTkMessagebox\" --add-data "c:\<python_path>\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:<br>
```3010399939,2976006537,2118338989,2113146805``` or <br>
```3010399939,2976006537,2118338989```
or <br>
```3010399939
2976006537
2118338989

3485
boiiiwd.py

File diff suppressed because it is too large Load Diff

View File

@ -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}<br>"
f"Type: {map_mod_type}<br>"
f"Size: {map_size}<br>"
)
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 = "<font color='black'>Toggle SteamCMD console\nPlease don't close the Console If you want to stop press the Stop boutton.</font>"
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_())

View File

@ -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()

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -8,4 +8,3 @@ Homepage: https://github.com/Akascape/CTkToolTip
__version__ = '0.8'
from .ctk_tooltip import CTkToolTip

View File

@ -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')]

View File

@ -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,9 +329,20 @@ 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)
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:
@ -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

View File

@ -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')
VERSION = "v0.3.1"

View File

@ -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("<KeyRelease>", 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("<Control-Button-1>", lambda event, label=label: self.copy_to_clipboard(label, workshop_id, event, append=True))
label.bind("<Button-2>", lambda event: self.open_folder_location(folder, event))
label.bind("<Button-3>", 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,22 +196,146 @@ 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():
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") 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}"
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()
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")
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))
text_to_add = f"{name} | Type: {item_type.capitalize()}"
@ -111,20 +343,39 @@ class LibraryTab(ctk.CTkScrollableFrame):
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 not self.added_items:
self.show_no_items_message()
if creation_timestamp is not None:
date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
else:
self.hide_no_items_message()
creation_timestamp = zone_path.stat().st_ctime
date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
def remove_item(self, item, folder):
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,23 +419,28 @@ 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
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
if online and valid_id!=False:
try:
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
response = requests.get(url)
@ -204,10 +466,15 @@ class LibraryTab(ctk.CTkScrollableFrame):
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", "Not a valid Workshop ID\nCouldn't get information.")
show_message("Warning", "Couldn't get information.")
for button_view in self.button_view_list:
button_view.configure(state="normal")
return
@ -235,10 +502,51 @@ class LibraryTab(ctk.CTkScrollableFrame):
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, invalid_warn, folder, description, online)
except requests.exceptions.RequestException as e:
show_message("Error", f"Failed to fetch map information.\nError: {e}", icon="cancel")
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
@ -247,14 +555,23 @@ class LibraryTab(ctk.CTkScrollableFrame):
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("<Button-1>", 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)
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("<Button-1>", 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=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
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="<Button-3>", 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')

View File

@ -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("<Button-1>", 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)
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("<Button-1>", 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=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
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,6 +1055,7 @@ 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 not update:
if self.already_installed:
item_ids = ", ".join(self.already_installed)
if self.settings_tab.skip_already_installed:
@ -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,6 +1380,7 @@ class BOIIIWD(ctk.CTk):
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")
@ -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")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -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)

View File

@ -0,0 +1,3 @@
import src.main as main
app = main.BOIIIWD()

View File

@ -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")

View File

@ -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",