Refactor project
4
.gitignore
vendored
@ -161,4 +161,6 @@ cython_debug/
|
||||
|
||||
# Other
|
||||
*.ini
|
||||
dist/
|
||||
*.conf
|
||||
dist/
|
||||
.vscode
|
10
README.md
@ -21,7 +21,7 @@
|
||||
|
||||
## Usage (script):
|
||||
- ```pip install -r requirements.txt``` -> use my modified [CTkToolTip](./CTkToolTip) and [CTkListbox](./CTkListbox) for [v0.2.8](https://github.com/faroukbmiled/BOIIIWD/releases) and up
|
||||
- ```python boiiiwd.py```
|
||||
- ```python boiiiwd_package\boiiiwd.py```
|
||||
- Slap in your workshop item link for example: "https://steamcommunity.com/sharedfiles/filedetails/?id=3011930738" or just the id 3011930738)
|
||||
|
||||
## Features:
|
||||
@ -36,16 +36,16 @@
|
||||
|
||||
<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
|
||||
|
@ -1,5 +1,4 @@
|
||||
from src import main
|
||||
import src.shared_vars as shared_vars
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = main.BOIIIWD()
|
||||
app.mainloop()
|
||||
shared_vars.app.mainloop()
|
||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 603 KiB After Width: | Height: | Size: 603 KiB |
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 326 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
@ -8,4 +8,3 @@ Homepage: https://github.com/Akascape/CTkToolTip
|
||||
__version__ = '0.8'
|
||||
|
||||
from .ctk_tooltip import CTkToolTip
|
||||
|
@ -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')]
|
@ -12,7 +12,7 @@ def check_config(name, fallback=None):
|
||||
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()
|
||||
@ -75,6 +75,56 @@ def create_update_script(current_exe, new_exe, updater_folder, program_name):
|
||||
|
||||
return script_path
|
||||
|
||||
def is_internet_available():
|
||||
try:
|
||||
requests.get("https://www.google.com", timeout=3)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def check_for_updates_func(window, ignore_up_todate=False):
|
||||
if not is_internet_available():
|
||||
show_message("Error!", "Internet connection is not available. Please check your internet connection and try again.")
|
||||
return
|
||||
try:
|
||||
latest_version = get_latest_release_version()
|
||||
current_version = VERSION
|
||||
int_latest_version = int(latest_version.replace("v", "").replace(".", ""))
|
||||
int_current_version = int(current_version.replace("v", "").replace(".", ""))
|
||||
|
||||
if latest_version and int_latest_version > int_current_version:
|
||||
msg_box = CTkMessagebox(title="Update Available", message=f"An update is available! Install now?\n\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="View", option_2="No", option_3="Yes", fade_in_duration=int(1), sound=True)
|
||||
|
||||
result = msg_box.get()
|
||||
|
||||
if result == "View":
|
||||
webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest")
|
||||
|
||||
if result == "Yes":
|
||||
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+)'
|
||||
@ -271,11 +321,22 @@ def get_button_state_colors(file_path, state):
|
||||
|
||||
def reset_steamcmd(no_warn=None):
|
||||
steamcmd_path = get_steamcmd_path()
|
||||
steamcmd_steamapps = os.path.join(steamcmd_path, "steamapps")
|
||||
if os.path.exists(steamcmd_steamapps):
|
||||
remove_tree(steamcmd_steamapps, show_error=True)
|
||||
if not no_warn:
|
||||
show_message("Success!", "SteamCMD has been reset successfully!", icon="info")
|
||||
|
||||
directories_to_reset = ["steamapps", "dumps", "logs", "depotcache", "appcache","userdata",]
|
||||
|
||||
for directory in directories_to_reset:
|
||||
directory_path = os.path.join(steamcmd_path, directory)
|
||||
if os.path.exists(directory_path):
|
||||
remove_tree(directory_path, show_error=True)
|
||||
|
||||
for root, _, files in os.walk(steamcmd_path):
|
||||
for filename in files:
|
||||
if filename.endswith((".old", ".crash")):
|
||||
file_path = os.path.join(root, filename)
|
||||
os.remove(file_path)
|
||||
|
||||
if not no_warn:
|
||||
show_message("Success!", "SteamCMD has been reset successfully!", icon="info")
|
||||
else:
|
||||
if not no_warn:
|
||||
show_message("Warning!", "steamapps folder was not found, maybe already removed?", icon="warning")
|
||||
@ -298,7 +359,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):
|
||||
ctk_tooltip.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
|
||||
|
@ -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 import ctk_listbox
|
||||
from .CTkToolTip import ctk_tooltip
|
||||
|
||||
|
||||
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"
|
@ -1,12 +1,14 @@
|
||||
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 shared
|
||||
|
||||
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,23 +17,33 @@ 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 = ctk_tooltip.CTkToolTip(self.update_button, message="Check items for updates", topmost=True)
|
||||
filter_tooltip = ctk_tooltip.CTkToolTip(self.filter_refresh_button, message="Refresh library", topmost=True)
|
||||
self.label_list = []
|
||||
self.button_list = []
|
||||
self.button_view_list = []
|
||||
self.file_cleaned = False
|
||||
self.filter_type = True
|
||||
self.clipboard_has_content = False
|
||||
self.item_block_list = set()
|
||||
self.added_folders = set()
|
||||
self.ids_added = set()
|
||||
|
||||
def add_item(self, item, image=None, 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_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")
|
||||
button.configure(command=lambda: self.remove_item(item, folder, workshop_id))
|
||||
button_view.configure(command=lambda: self.show_map_info(workshop_id, invalid_warn))
|
||||
button_view_tooltip = ctk_tooltip.CTkToolTip(button_view, message="Opens up a window that shows basic details")
|
||||
button_tooltip = ctk_tooltip.CTkToolTip(button, message="Removes the map/mod from your game")
|
||||
label.grid(row=len(self.label_list) + 1, column=0, pady=(0, 10), padx=(5, 10), sticky="w")
|
||||
button.grid(row=len(self.button_list) + 1, column=1, pady=(0, 10), padx=(50, 10), sticky="e")
|
||||
button_view.grid(row=len(self.button_view_list) + 1, column=1, pady=(0, 10), padx=(10, 75), sticky="w")
|
||||
@ -44,6 +56,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 = ctk_tooltip.CTkToolTip(label, message="Duplicated or Blocked item (Search item id in search)")
|
||||
|
||||
def on_label_hover(self, label, enter):
|
||||
if enter:
|
||||
@ -75,6 +89,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):
|
||||
@ -93,38 +199,158 @@ class LibraryTab(ctk.CTkScrollableFrame):
|
||||
mods_folder = Path(boiiiFolder) / "usermaps"
|
||||
mod_img = os.path.join(RESOURCES_DIR, "mod_image.png")
|
||||
map_img = os.path.join(RESOURCES_DIR, "map_image.png")
|
||||
b_mod_img = os.path.join(RESOURCES_DIR, "b_mod_image.png")
|
||||
b_map_img = os.path.join(RESOURCES_DIR, "b_map_image.png")
|
||||
map_count = 0
|
||||
mod_count = 0
|
||||
total_size = 0
|
||||
|
||||
folders_to_process = [mods_folder, maps_folder]
|
||||
|
||||
items_file = os.path.join(cwd(), LIBRARY_FILE)
|
||||
|
||||
for folder_path in folders_to_process:
|
||||
for zone_path in folder_path.glob("**/zone"):
|
||||
json_path = zone_path / "workshop.json"
|
||||
if json_path.exists():
|
||||
# current folder name
|
||||
curr_folder_name = zone_path.parent.name
|
||||
|
||||
workshop_id = extract_json_data(json_path, "PublisherID")
|
||||
name = extract_json_data(json_path, "Title").replace(">", "").replace("^", "")
|
||||
name = name[:45] + "..." if len(name) > 45 else name
|
||||
item_type = extract_json_data(json_path, "Type")
|
||||
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))
|
||||
folder_size_bytes = get_folder_size(zone_path.parent)
|
||||
size = convert_bytes_to_readable(folder_size_bytes)
|
||||
total_size += folder_size_bytes
|
||||
text_to_add = f"{name} | Type: {item_type.capitalize()}"
|
||||
mode_type = "ZM" if item_type == "map" and folder_name.startswith("zm") else "MP" if folder_name.startswith("mp") and item_type == "map" else None
|
||||
if mode_type:
|
||||
text_to_add += f" | Mode: {mode_type}"
|
||||
text_to_add += f" | ID: {workshop_id} | Size: {size}"
|
||||
if text_to_add not in self.added_items:
|
||||
self.added_items.add(text_to_add)
|
||||
image_path = mod_img if item_type == "mod" else map_img
|
||||
|
||||
self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), item_type=item_type, workshop_id=workshop_id, folder=zone_path.parent)
|
||||
creation_timestamp = None
|
||||
for ff_file in zone_path.glob("*.ff"):
|
||||
if ff_file.exists():
|
||||
creation_timestamp = ff_file.stat().st_ctime
|
||||
break
|
||||
|
||||
if creation_timestamp is not None:
|
||||
date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
|
||||
else:
|
||||
creation_timestamp = zone_path.stat().st_ctime
|
||||
date_added = datetime.fromtimestamp(creation_timestamp).strftime("%d %b, %Y @ %I:%M%p")
|
||||
|
||||
map_count += 1 if item_type == "map" else 0
|
||||
mod_count += 1 if item_type == "mod" else 0
|
||||
if curr_folder_name not in self.added_folders:
|
||||
image_path = mod_img if item_type == "mod" else map_img
|
||||
if not (str(curr_folder_name).strip() == str(workshop_id).strip() or str(curr_folder_name).strip() == str(folder_name).strip()):
|
||||
try: self.remove_item_by_option(items_file, curr_folder_name, "folder_name")
|
||||
except: pass
|
||||
self.item_block_list.add(curr_folder_name)
|
||||
image_path = b_mod_img if item_type == "mod" else b_map_img
|
||||
text_to_add += " | ⚠️"
|
||||
elif curr_folder_name not in self.added_folders and workshop_id in self.ids_added:
|
||||
try: self.remove_item_by_option(items_file, curr_folder_name, "folder_name")
|
||||
except: pass
|
||||
image_path = b_mod_img if item_type == "mod" else b_map_img
|
||||
text_to_add += " | ⚠️"
|
||||
|
||||
self.added_items.add(text_to_add)
|
||||
if image_path is b_mod_img or image_path is b_map_img:
|
||||
self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), workshop_id=workshop_id, folder=zone_path.parent, invalid_warn=True)
|
||||
else:
|
||||
self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), workshop_id=workshop_id, folder=zone_path.parent)
|
||||
id_found, folder_found = self.item_exists_in_file(items_file, workshop_id, curr_folder_name)
|
||||
item_info = {
|
||||
"id": workshop_id,
|
||||
"text": text_to_add,
|
||||
"date": date_added,
|
||||
"folder_name": curr_folder_name,
|
||||
"json_folder_name": folder_name
|
||||
}
|
||||
# when item is blocked ,item_exists_in_file() returns None for folder_found
|
||||
if not id_found and folder_found == None:
|
||||
self.remove_item_by_option(items_file, curr_folder_name, "folder_name")
|
||||
elif not id_found and not folder_found and curr_folder_name not in self.item_block_list and workshop_id not in self.ids_added:
|
||||
if not os.path.exists(items_file):
|
||||
with open(items_file, "w") as f:
|
||||
json.dump([item_info], f, indent=4)
|
||||
else:
|
||||
with open(items_file, "r+") as f:
|
||||
items_data = json.load(f)
|
||||
items_data.append(item_info)
|
||||
f.seek(0)
|
||||
json.dump(items_data, f, indent=4)
|
||||
|
||||
if id_found and not folder_found and curr_folder_name not in self.item_block_list and workshop_id not in self.ids_added:
|
||||
self.update_or_add_item_by_id(items_file, item_info, workshop_id)
|
||||
|
||||
# keep here cuz of item_exists_in_file() testing
|
||||
self.added_folders.add(curr_folder_name)
|
||||
if not workshop_id in self.ids_added:
|
||||
self.ids_added.add(workshop_id)
|
||||
|
||||
if not self.file_cleaned and os.path.exists(items_file):
|
||||
self.file_cleaned = True
|
||||
self.clean_json_file(items_file)
|
||||
|
||||
if not self.added_items:
|
||||
self.show_no_items_message()
|
||||
else:
|
||||
self.hide_no_items_message()
|
||||
|
||||
def remove_item(self, item, folder):
|
||||
if map_count > 0 or mod_count > 0:
|
||||
return f"Maps: {map_count} - Mods: {mod_count} - Total size: {convert_bytes_to_readable(total_size)}"
|
||||
return "No items in current selected folder"
|
||||
|
||||
def update_item(self, boiiiFolder, id, item_type, foldername):
|
||||
try:
|
||||
if item_type == "map":
|
||||
folder_path = Path(boiiiFolder) / "usermaps" / f"{foldername}"
|
||||
elif item_type == "mod":
|
||||
folder_path = Path(boiiiFolder) / "mods" / f"{foldername}"
|
||||
else:
|
||||
raise ValueError("Unsupported item_type. It must be 'map' or 'mod'.")
|
||||
|
||||
for zone_path in folder_path.glob("**/zone"):
|
||||
json_path = zone_path / "workshop.json"
|
||||
if json_path.exists():
|
||||
workshop_id = extract_json_data(json_path, "PublisherID")
|
||||
if workshop_id == id:
|
||||
name = extract_json_data(json_path, "Title").replace(">", "").replace("^", "")
|
||||
name = name[:45] + "..." if len(name) > 45 else name
|
||||
item_type = extract_json_data(json_path, "Type")
|
||||
folder_name = extract_json_data(json_path, "FolderName")
|
||||
size = convert_bytes_to_readable(get_folder_size(zone_path.parent))
|
||||
text_to_add = f"{name} | Type: {item_type.capitalize()}"
|
||||
mode_type = "ZM" if item_type == "map" and folder_name.startswith("zm") else "MP" if folder_name.startswith("mp") and item_type == "map" else None
|
||||
if mode_type:
|
||||
text_to_add += f" | Mode: {mode_type}"
|
||||
text_to_add += f" | ID: {workshop_id} | Size: {size}"
|
||||
date_added = datetime.now().strftime("%d %b, %Y @ %I:%M%p")
|
||||
items_file = os.path.join(cwd(), LIBRARY_FILE)
|
||||
|
||||
item_info = {
|
||||
"id": workshop_id,
|
||||
"text": text_to_add,
|
||||
"date": date_added,
|
||||
"folder_name": foldername,
|
||||
"json_folder_name": folder_name
|
||||
}
|
||||
self.update_or_add_item_by_id(items_file, item_info, id)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
show_message("Error updating json file", f"Error while updating library json file\n{e}")
|
||||
|
||||
def remove_item(self, item, folder, id):
|
||||
items_file = os.path.join(cwd(), LIBRARY_FILE)
|
||||
for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list):
|
||||
if item == label.cget("text"):
|
||||
self.added_folders.remove(os.path.basename(folder))
|
||||
try:
|
||||
shutil.rmtree(folder)
|
||||
except Exception as e:
|
||||
@ -136,7 +362,9 @@ 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):
|
||||
for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list):
|
||||
@ -147,8 +375,10 @@ 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()
|
||||
status = self.load_items(shared.app.edit_destination_folder.get().strip())
|
||||
shared.app.title(f"BOIII Workshop Downloader - Library ➜ {status}")
|
||||
|
||||
def view_item(self, workshop_id):
|
||||
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
|
||||
@ -163,7 +393,7 @@ class LibraryTab(ctk.CTkScrollableFrame):
|
||||
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, invalid_warn=False):
|
||||
for button_view in self.button_view_list:
|
||||
button_view.configure(state="disabled")
|
||||
|
||||
@ -235,7 +465,7 @@ 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)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
show_message("Error", f"Failed to fetch map information.\nError: {e}", icon="cancel")
|
||||
@ -247,14 +477,16 @@ 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):
|
||||
def main_thread():
|
||||
try:
|
||||
items_file = os.path.join(cwd(), LIBRARY_FILE)
|
||||
top = ctk.CTkToplevel(self)
|
||||
if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")):
|
||||
top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")))
|
||||
top.title("Map/Mod Information")
|
||||
top.attributes('-topmost', 'true')
|
||||
down_date = self.get_item_by_id(items_file, workshop_id, 'date')
|
||||
|
||||
def close_window():
|
||||
top.destroy()
|
||||
@ -262,6 +494,25 @@ class LibraryTab(ctk.CTkScrollableFrame):
|
||||
def view_map_mod():
|
||||
webbrowser.open(url)
|
||||
|
||||
def check_for_updates():
|
||||
try:
|
||||
|
||||
if check_item_date(down_date, date_updated):
|
||||
if show_message("There is an update.", "Press download to redownload!", icon="info", _return=True, option_1="No", option_2="Download"):
|
||||
if shared.app.is_downloading:
|
||||
show_message("Error", "Please wait for the current download to finish or stop it then restart.", icon="cancel")
|
||||
return
|
||||
shared.app.edit_workshop_id.delete(0, "end")
|
||||
shared.app.edit_workshop_id.insert(0, workshop_id)
|
||||
shared.app.main_button_event()
|
||||
shared.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")
|
||||
@ -293,6 +544,9 @@ class LibraryTab(ctk.CTkScrollableFrame):
|
||||
date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated}")
|
||||
date_updated_label.grid(row=4, column=0, columnspan=2, sticky="w", padx=20, pady=5)
|
||||
|
||||
date_updated_label = ctk.CTkLabel(info_frame, text=f"Downloaded at: {down_date}")
|
||||
date_updated_label.grid(row=5, column=0, columnspan=2, sticky="w", padx=20, pady=5)
|
||||
|
||||
stars_image_label = ctk.CTkLabel(stars_frame)
|
||||
stars_width, stars_height = stars_image_size
|
||||
stars_image_widget = ctk.CTkImage(stars_image, size=(int(stars_width), int(stars_height)))
|
||||
@ -310,11 +564,19 @@ class LibraryTab(ctk.CTkScrollableFrame):
|
||||
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))
|
||||
close_button = ctk.CTkButton(buttons_frame, text="View", command=view_map_mod, width=130)
|
||||
close_button.grid(row=0, column=0, padx=(20, 20), pady=(10, 10), sticky="n")
|
||||
|
||||
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 = ctk_tooltip.CTkToolTip(update_btn, message="Checks and installs updates of the current selected item (redownload!)", topmost=True)
|
||||
|
||||
view_button = ctk.CTkButton(buttons_frame, text="Close", command=close_window, width=130)
|
||||
view_button.grid(row=0, column=2, padx=(10, 20), pady=(10, 10), sticky="n")
|
||||
|
||||
if invalid_warn:
|
||||
update_btn.configure(text="Update", state="disabled")
|
||||
update_btn_tooltip.configure(message="Disabled due to item being blocked or duplicated")
|
||||
|
||||
top.grid_rowconfigure(0, weight=0)
|
||||
top.grid_rowconfigure(1, weight=0)
|
||||
@ -322,7 +584,206 @@ 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)
|
||||
|
||||
def check_for_updates(self, on_launch=False):
|
||||
if not is_internet_available():
|
||||
show_message("Error!", "Internet connection is not available. Please check your internet connection and try again.")
|
||||
return
|
||||
self.after(1, self.update_button.configure(state="disabled"))
|
||||
self.update_tooltip.configure(message='Still loading please wait...')
|
||||
cevent = Event()
|
||||
cevent.x_root = self.update_button.winfo_rootx()
|
||||
cevent.y_root = self.update_button.winfo_rooty()
|
||||
if not on_launch:
|
||||
show_noti(self.update_button, "Please wait, window will popup shortly", event=cevent, noti_dur=3.0, topmost=True)
|
||||
threading.Thread(target=self.check_items_func, args=(on_launch,)).start()
|
||||
|
||||
def items_update_message(self, to_update_len):
|
||||
def main_thread():
|
||||
if show_message(f"{to_update_len} Item updates available", f"{to_update_len} Workshop Items have an update, Would you like to open the item updater window?", icon="info", _return=True):
|
||||
shared.app.after(1, self.update_items_window)
|
||||
else:
|
||||
return
|
||||
shared.app.after(0, main_thread)
|
||||
self.update_button.configure(state="normal", width=65, height=20)
|
||||
self.update_tooltip.configure(message='Check items for updates')
|
||||
return
|
||||
|
||||
def check_items_func(self, on_launch):
|
||||
# Needed to refresh item that needs updates
|
||||
self.to_update.clear()
|
||||
|
||||
def if_id_needs_update(item_id, item_date, text):
|
||||
try:
|
||||
headers = {'Cache-Control': 'no-cache'}
|
||||
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={item_id}"
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
content = response.text
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
details_stats_container = soup.find("div", class_="detailsStatsContainerRight")
|
||||
details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight")
|
||||
try:
|
||||
date_updated = details_stat_elements[2].text.strip()
|
||||
except:
|
||||
try:
|
||||
date_updated = details_stat_elements[1].text.strip()
|
||||
except:
|
||||
return False
|
||||
|
||||
if check_item_date(item_date, date_updated):
|
||||
self.to_update.add(text + f" | Updated: {date_updated}")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
show_message("Error", f"Error occured\n{e}", icon="cancel")
|
||||
return
|
||||
|
||||
def check_for_update():
|
||||
lib_data = None
|
||||
|
||||
if not os.path.exists(os.path.join(cwd(), LIBRARY_FILE)):
|
||||
show_message("Error checking for item updates! -> Setting is on", "Please visit library tab at least once with the correct boiii path!, you also need to have at lease 1 item!")
|
||||
return
|
||||
|
||||
with open(LIBRARY_FILE, 'r') as file:
|
||||
lib_data = json.load(file)
|
||||
|
||||
for item in lib_data:
|
||||
item_id = item["id"]
|
||||
item_date = item["date"]
|
||||
if_id_needs_update(item_id, item_date, item["text"])
|
||||
|
||||
check_for_update()
|
||||
|
||||
to_update_len = len(self.to_update)
|
||||
if to_update_len > 0:
|
||||
self.items_update_message(to_update_len)
|
||||
else:
|
||||
self.update_button.configure(state="normal", width=65, height=20)
|
||||
self.update_tooltip.configure(message='Check items for updates')
|
||||
if not on_launch:
|
||||
show_message("No updates found!", "Items are up to date!", icon="info")
|
||||
|
||||
def update_items_window(self):
|
||||
try:
|
||||
top = ctk.CTkToplevel(master=None)
|
||||
top.withdraw()
|
||||
if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")):
|
||||
top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")))
|
||||
top.title("Item updater - List of Items with Updates - Click to select 1 or more")
|
||||
longest_text_length = max(len(text) for text in self.to_update)
|
||||
window_width = longest_text_length * 6 + 5
|
||||
top.geometry(f"{window_width}x450")
|
||||
top.attributes('-topmost', 'true')
|
||||
top.resizable(True, True)
|
||||
selected_id_list = []
|
||||
cevent = Event()
|
||||
self.select_all_bool = False
|
||||
|
||||
listbox = ctk_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 shared.app.is_downloading:
|
||||
show_message("Error", "Please wait for the current download to finish or stop it then start.", icon="cancel")
|
||||
return
|
||||
shared.app.edit_workshop_id.delete(0, "end")
|
||||
shared.app.edit_workshop_id.insert(0, selected_id_list[0])
|
||||
shared.app.main_button_event()
|
||||
shared.app.download_map(update=True)
|
||||
top.destroy()
|
||||
return
|
||||
|
||||
elif len(selected_id_list) > 1:
|
||||
if shared.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)
|
||||
shared.app.queuetextarea.delete("1.0", "end")
|
||||
shared.app.queuetextarea.insert("1.0", comma_separated_ids)
|
||||
shared.app.queue_button_event()
|
||||
shared.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')
|
||||
|
@ -1,9 +1,9 @@
|
||||
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 src.helpers import *
|
||||
|
||||
from src.library_tab import LibraryTab
|
||||
from src.settings_tab import SettingsTab
|
||||
from src.update_window import UpdateWindow
|
||||
|
||||
|
||||
def check_for_updates_func(window, ignore_up_todate=False):
|
||||
try:
|
||||
@ -20,7 +20,6 @@ def check_for_updates_func(window, ignore_up_todate=False):
|
||||
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()
|
||||
@ -53,7 +52,7 @@ class BOIIIWD(ctk.CTk):
|
||||
# self.app_instance = BOIIIWD()
|
||||
|
||||
# configure window
|
||||
self.title("BOIII Workshop Downloader - Main")
|
||||
self.title("boiii Workshop Downloader - Main")
|
||||
|
||||
try:
|
||||
geometry_file = os.path.join(cwd(), "boiiiwd_dont_touch.conf")
|
||||
@ -185,15 +184,15 @@ class BOIIIWD(ctk.CTk):
|
||||
|
||||
self.button_browse = ctk.CTkButton(master=self.optionsframe, text="Workshop", command=self.open_browser, width=10)
|
||||
self.button_browse.grid(row=2, column=5, padx=(0, 20), pady=(0, 10), sticky="en")
|
||||
self.button_browse_tooltip = CTkToolTip(self.button_browse, message="Will open steam workshop for boiii in your browser")
|
||||
self.button_browse_tooltip = ctk_tooltip.CTkToolTip(self.button_browse, message="Will open steam workshop for boiii in your browser")
|
||||
|
||||
self.info_button = ctk.CTkButton(master=self.optionsframe, text="Details", command=self.show_map_info, width=10)
|
||||
self.info_button.grid(row=2, column=5, padx=(0, 20), pady=(0, 10), sticky="wn")
|
||||
|
||||
self.label_destination_folder = ctk.CTkLabel(master=self.optionsframe, text="Enter Your BOIII folder:")
|
||||
self.label_destination_folder = 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)
|
||||
@ -232,9 +231,9 @@ class BOIIIWD(ctk.CTk):
|
||||
self.sidebar_queue.configure(text="Queue 🚧", command=self.queue_button_event)
|
||||
sidebar_settings_button_image = os.path.join(RESOURCES_DIR, "sett10.png")
|
||||
self.sidebar_settings.configure(command=self.settings_button_event, text="", image=ctk.CTkImage(Image.open(sidebar_settings_button_image), size=(int(35), int(35))), fg_color="transparent", width=45, height=45)
|
||||
self.sidebar_settings_tooltip = CTkToolTip(self.sidebar_settings, message="Settings")
|
||||
self.sidebar_library_tooltip = CTkToolTip(self.sidebar_library, message="Experimental")
|
||||
self.sidebar_queue_tooltip = CTkToolTip(self.sidebar_queue, message="Experimental")
|
||||
self.sidebar_settings_tooltip = ctk_tooltip.CTkToolTip(self.sidebar_settings, message="Settings")
|
||||
self.sidebar_library_tooltip = ctk_tooltip.CTkToolTip(self.sidebar_library, message="Experimental")
|
||||
self.sidebar_queue_tooltip = ctk_tooltip.CTkToolTip(self.sidebar_queue, message="Experimental")
|
||||
self.bind("<Configure>", self.save_window_size)
|
||||
|
||||
# context_menus
|
||||
@ -271,6 +270,10 @@ class BOIIIWD(ctk.CTk):
|
||||
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()
|
||||
@ -406,8 +409,9 @@ class BOIIIWD(ctk.CTk):
|
||||
|
||||
def show_library_widgets(self):
|
||||
self.title("BOIII Workshop Downloader - Library")
|
||||
self.library_tab.load_items(self.edit_destination_folder.get())
|
||||
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")
|
||||
@ -521,7 +525,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)
|
||||
@ -613,8 +617,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
|
||||
|
||||
@ -804,14 +809,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 +853,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 +864,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 +899,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 +908,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 +946,7 @@ class BOIIIWD(ctk.CTk):
|
||||
return
|
||||
self.after(0, callback)
|
||||
|
||||
def download_map(self):
|
||||
def download_map(self, update=False):
|
||||
self.is_downloading = False
|
||||
self.fail_threshold = 0
|
||||
if not self.is_pressed:
|
||||
@ -948,15 +955,15 @@ class BOIIIWD(ctk.CTk):
|
||||
self.library_tab.load_items(self.edit_destination_folder.get())
|
||||
if self.queue_enabled:
|
||||
self.item_skipped = False
|
||||
start_down_thread = threading.Thread(target=self.queue_download_thread)
|
||||
start_down_thread = threading.Thread(target=self.queue_download_thread, args=(update,))
|
||||
start_down_thread.start()
|
||||
else:
|
||||
start_down_thread = threading.Thread(target=self.download_thread)
|
||||
start_down_thread = threading.Thread(target=self.download_thread, args=(update,))
|
||||
start_down_thread.start()
|
||||
else:
|
||||
show_message("Warning", "Already pressed, Please wait.")
|
||||
|
||||
def queue_download_thread(self):
|
||||
def queue_download_thread(self, update=None):
|
||||
self.stopped = False
|
||||
self.queue_stop_button = False
|
||||
try:
|
||||
@ -1032,18 +1039,19 @@ class BOIIIWD(ctk.CTk):
|
||||
if any(workshop_id in item for item in self.library_tab.added_items):
|
||||
self.already_installed.append(workshop_id)
|
||||
|
||||
if self.already_installed:
|
||||
item_ids = ", ".join(self.already_installed)
|
||||
if self.settings_tab.skip_already_installed:
|
||||
for item in self.already_installed:
|
||||
if item in items:
|
||||
items.remove(item)
|
||||
show_message("Heads up!, map/s skipped => skip is on in settings", f"These item IDs may already be installed and are skipped:\n{item_ids}", icon="info")
|
||||
if not any(isinstance(item, int) for item in items):
|
||||
self.stop_download()
|
||||
return
|
||||
else:
|
||||
show_message("Heads up! map/s not skipped => skip is off in settings", f"These item IDs may already be installed:\n{item_ids}", icon="info")
|
||||
if not update:
|
||||
if self.already_installed:
|
||||
item_ids = ", ".join(self.already_installed)
|
||||
if self.settings_tab.skip_already_installed:
|
||||
for item in self.already_installed:
|
||||
if item in items:
|
||||
items.remove(item)
|
||||
show_message("Heads up!, map/s skipped => skip is on in settings", f"These item IDs may already be installed and are skipped:\n{item_ids}", icon="info")
|
||||
if not any(isinstance(item, int) for item in items):
|
||||
self.stop_download()
|
||||
return
|
||||
else:
|
||||
show_message("Heads up! map/s not skipped => skip is off in settings", f"These item IDs may already be installed:\n{item_ids}", icon="info")
|
||||
|
||||
self.after(1, self.status_text.configure(text=f"Status: Total size: ~{convert_bytes_to_readable(self.total_queue_size)}"))
|
||||
start_time = time.time()
|
||||
@ -1210,6 +1218,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,7 +1229,21 @@ 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(cwd(), LIBRARY_FILE)
|
||||
if self.library_tab.item_exists_in_file(items_file, workshop_id):
|
||||
get_folder_name = self.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name")
|
||||
if get_folder_name:
|
||||
folder_name = get_folder_name
|
||||
else:
|
||||
try:
|
||||
folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
|
||||
except:
|
||||
folder_name = extract_json_data(json_file_path, "publisherID")
|
||||
else:
|
||||
try:
|
||||
folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
|
||||
except:
|
||||
folder_name = extract_json_data(json_file_path, "publisherID")
|
||||
|
||||
if mod_type == "mod":
|
||||
mods_folder = os.path.join(destination_folder, "mods")
|
||||
@ -1243,6 +1266,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 +1300,7 @@ class BOIIIWD(ctk.CTk):
|
||||
self.stop_download()
|
||||
self.is_pressed = False
|
||||
|
||||
def download_thread(self):
|
||||
def download_thread(self, update=None):
|
||||
try:
|
||||
self.settings_tab.stopped = False
|
||||
|
||||
@ -1332,12 +1357,13 @@ class BOIIIWD(ctk.CTk):
|
||||
self.stop_download()
|
||||
return
|
||||
|
||||
if any(workshop_id in item for item in self.library_tab.added_items):
|
||||
if self.settings_tab.skip_already_installed:
|
||||
show_message("Heads up!, map skipped => Skip is on in settings", f"This item may already be installed, Stopping: {workshop_id}", icon="info")
|
||||
self.stop_download()
|
||||
return
|
||||
show_message("Heads up! map not skipped => Skip is off in settings", f"This item may already be installed: {workshop_id}", icon="info")
|
||||
if not update:
|
||||
if any(workshop_id in item for item in self.library_tab.added_items):
|
||||
if self.settings_tab.skip_already_installed:
|
||||
show_message("Heads up!, map skipped => Skip is on in settings", f"This item may already be installed, Stopping: {workshop_id}", icon="info")
|
||||
self.stop_download()
|
||||
return
|
||||
show_message("Heads up! map not skipped => Skip is off in settings", f"This item may already be installed: {workshop_id}", icon="info")
|
||||
|
||||
self.after(1, lambda mid=workshop_id: self.label_file_size.configure(text=f"File size: {get_workshop_file_size(mid ,raw=True)}"))
|
||||
download_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "downloads", "311210", workshop_id)
|
||||
@ -1462,7 +1488,21 @@ 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(cwd(), LIBRARY_FILE)
|
||||
if self.library_tab.item_exists_in_file(items_file, workshop_id):
|
||||
get_folder_name = self.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name")
|
||||
if get_folder_name:
|
||||
folder_name = get_folder_name
|
||||
else:
|
||||
try:
|
||||
folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
|
||||
except:
|
||||
folder_name = extract_json_data(json_file_path, "publisherID")
|
||||
else:
|
||||
try:
|
||||
folder_name = extract_json_data(json_file_path, self.settings_tab.folder_options.get())
|
||||
except:
|
||||
folder_name = extract_json_data(json_file_path, "publisherID")
|
||||
|
||||
if mod_type == "mod":
|
||||
mods_folder = os.path.join(destination_folder, "mods")
|
||||
@ -1486,6 +1526,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")
|
||||
@ -1552,3 +1593,7 @@ class BOIIIWD(ctk.CTk):
|
||||
self.after(50, self.status_text.configure(text=f"Status: Standby!"))
|
||||
self.after(1, self.label_speed.configure(text=f"Awaiting Download!"))
|
||||
self.skip_boutton.grid_remove()
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app = BOIIIWD()
|
||||
# app.mainloop()
|
Before Width: | Height: | Size: 4.2 KiB |
@ -1,6 +1,7 @@
|
||||
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 *
|
||||
|
||||
import src.shared_vars as shared
|
||||
|
||||
def check_for_updates_func(window, ignore_up_todate=False):
|
||||
try:
|
||||
@ -57,6 +58,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)
|
||||
@ -82,7 +84,7 @@ class SettingsTab(ctk.CTkFrame):
|
||||
self.console_var.trace_add("write", self.enable_save_button)
|
||||
self.checkbox_show_console = ctk.CTkSwitch(left_frame, text="Console (On Download)", variable=self.console_var)
|
||||
self.checkbox_show_console.grid(row=1, column=1, padx=20, pady=(20, 0), sticky="nw")
|
||||
self.checkbox_show_console_tooltip = CTkToolTip(self.checkbox_show_console, message="Toggle SteamCMD console\nPlease don't close the Console If you want to stop press the Stop button")
|
||||
self.checkbox_show_console_tooltip = ctk_tooltip.CTkToolTip(self.checkbox_show_console, message="Toggle SteamCMD console\nPlease don't close the Console If you want to stop press the Stop button")
|
||||
self.console_var.set(self.load_settings("console"))
|
||||
|
||||
# Show continuous checkbox
|
||||
@ -90,7 +92,7 @@ class SettingsTab(ctk.CTkFrame):
|
||||
self.continuous_var.trace_add("write", self.enable_save_button)
|
||||
self.checkbox_continuous = ctk.CTkSwitch(left_frame, text="Continuous Download", variable=self.continuous_var)
|
||||
self.checkbox_continuous.grid(row=2, column=1, padx=20, pady=(20, 0), sticky="nw")
|
||||
self.checkbox_continuous_tooltip = CTkToolTip(self.checkbox_continuous, message="This will make sure that the download restarts and resumes! until it finishes if steamcmd crashes randomly (it will not redownload from the start)")
|
||||
self.checkbox_continuous_tooltip = ctk_tooltip.CTkToolTip(self.checkbox_continuous, message="This will make sure that the download restarts and resumes! until it finishes if steamcmd crashes randomly (it will not redownload from the start)")
|
||||
self.continuous_var.set(self.load_settings("continuous_download"))
|
||||
|
||||
# clean on finish checkbox
|
||||
@ -98,7 +100,7 @@ class SettingsTab(ctk.CTkFrame):
|
||||
self.clean_checkbox_var.trace_add("write", self.enable_save_button)
|
||||
self.clean_checkbox = ctk.CTkSwitch(left_frame, text="Clean on finish", variable=self.clean_checkbox_var)
|
||||
self.clean_checkbox.grid(row=3, column=1, padx=20, pady=(20, 0), sticky="nw")
|
||||
self.clean_checkbox_tooltip = CTkToolTip(self.clean_checkbox, message="Cleans the map that have been downloaded and installed from steamcmd's steamapps folder ,to save space")
|
||||
self.clean_checkbox_tooltip = ctk_tooltip.CTkToolTip(self.clean_checkbox, message="Cleans the map that have been downloaded and installed from steamcmd's steamapps folder ,to save space")
|
||||
self.clean_checkbox_var.set(self.load_settings("clean_on_finish", "on"))
|
||||
|
||||
# Show estimated_progress checkbox
|
||||
@ -106,36 +108,53 @@ class SettingsTab(ctk.CTkFrame):
|
||||
self.estimated_progress_var.trace_add("write", self.enable_save_button)
|
||||
self.estimated_progress_cb = ctk.CTkSwitch(left_frame, text="Estimated Progress Bar", variable=self.estimated_progress_var)
|
||||
self.estimated_progress_cb.grid(row=4, column=1, padx=20, pady=(20, 0), sticky="nw")
|
||||
self.estimated_progress_var_tooltip = CTkToolTip(self.estimated_progress_cb, message="This will change how to progress bar works by estimating how long the download will take\
|
||||
self.estimated_progress_var_tooltip = ctk_tooltip.CTkToolTip(self.estimated_progress_cb, message="This will change how to progress bar works by estimating how long the download will take\
|
||||
\nThis is not accurate ,it's better than with it off which is calculating the downloaded folder size which steamcmd dumps the full size rigth mostly")
|
||||
self.estimated_progress_var.set(self.load_settings("estimated_progress", "on"))
|
||||
|
||||
# Show show fails checkbox
|
||||
self.show_fails_var = ctk.BooleanVar()
|
||||
self.show_fails_var.trace_add("write", self.enable_save_button)
|
||||
self.show_fails_cb = ctk.CTkSwitch(left_frame, text="Show fails (on top of progress bar):", variable=self.show_fails_var)
|
||||
self.show_fails_cb = 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.show_fails_tooltip = ctk_tooltip.CTkToolTip(self.show_fails_cb, message="Display how many times steamcmd has failed/crashed\nIf the number is getting high quickly then try pressing Reset SteamCMD and try again, otherwise its fine")
|
||||
self.estimated_progress_var.set(self.load_settings("show_fails", "on"))
|
||||
|
||||
# Show skip_already_installed maps checkbox
|
||||
self.skip_already_installed_var = ctk.BooleanVar()
|
||||
self.skip_already_installed_var.trace_add("write", self.enable_save_button)
|
||||
self.skip_already_installed_ch = ctk.CTkSwitch(left_frame, text="Skip already installed maps:", variable=self.skip_already_installed_var)
|
||||
self.skip_already_installed_ch = 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_ch_tooltip = ctk_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 = ctk_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_tooltip = CTkToolTip(self.reset_steamcmd_on_fail, message="This actually fixes steamcmd when its crashing way too much")
|
||||
self.reset_steamcmd_on_fail.grid(row=8, column=1, padx=(190, 0), pady=(10, 0), sticky="nw")
|
||||
self.reset_steamcmd_on_fail_tooltip = ctk_tooltip.CTkToolTip(self.reset_steamcmd_on_fail, message="This actually fixes steamcmd when its crashing way too much")
|
||||
self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "10"))
|
||||
|
||||
# item folder naming
|
||||
self.folder_options_label_var = ctk.IntVar()
|
||||
self.folder_options_label_var.trace_add("write", self.enable_save_button)
|
||||
self.folder_options_label = ctk.CTkLabel(left_frame, text="Items Folder Naming:", anchor="nw")
|
||||
self.folder_options_label.grid(row=10, column=1, padx=20, pady=(10, 0), sticky="nw")
|
||||
self.folder_options = ctk.CTkOptionMenu(left_frame, values=["PublisherID", "FolderName"], variable=self.folder_options_label_var)
|
||||
self.folder_options.grid(row=10, column=1, padx=(150, 0), pady=(3, 0), sticky="nw")
|
||||
self.folder_options.set(value=self.load_settings("folder_naming", "PublisherID"))
|
||||
|
||||
# Check for updates button n Launch boiii
|
||||
self.check_for_updates = ctk.CTkButton(right_frame, text="Check for updates", command=self.settings_check_for_updates)
|
||||
self.check_for_updates.grid(row=1, column=1, padx=20, pady=(20, 0), sticky="n")
|
||||
@ -145,28 +164,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 = ctk_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 = ctk_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
|
||||
@ -238,6 +261,17 @@ class SettingsTab(ctk.CTkFrame):
|
||||
|
||||
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 +329,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
|
||||
@ -381,7 +421,7 @@ class SettingsTab(ctk.CTkFrame):
|
||||
def boiiiwd_custom_theme(self, disable_only=None):
|
||||
file_to_rename = os.path.join(cwd(), "boiiiwd_theme.json")
|
||||
if os.path.exists(file_to_rename):
|
||||
timestamp = datetime.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))
|
||||
|
||||
@ -417,3 +457,205 @@ class SettingsTab(ctk.CTkFrame):
|
||||
|
||||
def settings_reset_steamcmd(self):
|
||||
reset_steamcmd()
|
||||
|
||||
def from_steam_to_boiii_toplevel(self):
|
||||
def main_thread():
|
||||
try:
|
||||
top = ctk.CTkToplevel(self)
|
||||
if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")):
|
||||
top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")))
|
||||
top.title("Steam to boiii -> Workshop items")
|
||||
top.attributes('-topmost', 'true')
|
||||
top.resizable(False, False)
|
||||
# Create input boxes
|
||||
center_frame = ctk.CTkFrame(top)
|
||||
center_frame.grid(row=0, column=0, padx=20, pady=20)
|
||||
|
||||
# Create input boxes
|
||||
steam_folder_label = ctk.CTkLabel(center_frame, text="Steam Folder:")
|
||||
steam_folder_label.grid(row=0, column=0, padx=(20, 20), pady=(10, 0), sticky='w')
|
||||
steam_folder_entry = ctk.CTkEntry(center_frame, width=225)
|
||||
steam_folder_entry.grid(row=1, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes')
|
||||
button_steam_browse = ctk.CTkButton(center_frame, text="Select", width=10)
|
||||
button_steam_browse.grid(row=1, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes")
|
||||
|
||||
boiii_folder_label = ctk.CTkLabel(center_frame, text="boiii Folder:")
|
||||
boiii_folder_label.grid(row=2, column=0, padx=(20, 20), pady=(10, 0), sticky='w')
|
||||
boiii_folder_entry = ctk.CTkEntry(center_frame, width=225)
|
||||
boiii_folder_entry.grid(row=3, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes')
|
||||
button_BOIII_browse = ctk.CTkButton(center_frame, text="Select", width=10)
|
||||
button_BOIII_browse.grid(row=3, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes")
|
||||
|
||||
# Create option to choose between cut or copy
|
||||
operation_label = ctk.CTkLabel(center_frame, text="Choose operation:")
|
||||
operation_label.grid(row=4, column=0, padx=(20, 20), pady=(10, 10), sticky='wnes')
|
||||
copy_var = ctk.BooleanVar()
|
||||
cut_var = ctk.BooleanVar()
|
||||
copy_check = ctk.CTkCheckBox(center_frame, text="Copy", variable=copy_var)
|
||||
cut_check = ctk.CTkCheckBox(center_frame, text="Cut", variable=cut_var)
|
||||
copy_check.grid(row=4, column=1, padx=(0, 10), pady=(10, 10), sticky='wnes')
|
||||
cut_check.grid(row=4, column=2, padx=(0, 10), pady=(10, 10), sticky='nes')
|
||||
|
||||
# Create progress bar
|
||||
progress_bar = ctk.CTkProgressBar(center_frame, mode="determinate", height=20, corner_radius=7)
|
||||
progress_bar.grid(row=5, column=0, columnspan=3, padx=(20, 20), pady=(10, 10), sticky='wnes')
|
||||
progress_text = ctk.CTkLabel(progress_bar, text="0%", font=("Helvetica", 12), fg_color="transparent", text_color="white", height=0, width=0, corner_radius=0)
|
||||
progress_text.place(relx=0.5, rely=0.5, anchor="center")
|
||||
|
||||
copy_button = ctk.CTkButton(center_frame, text="Start (Copy)")
|
||||
copy_button.grid(row=6, column=0, columnspan=3,padx=(20, 20), pady=(10, 10), sticky='wnes')
|
||||
|
||||
# funcs
|
||||
# had to use this shit again cuz of threading issues with widgets
|
||||
def copy_with_progress(src, dst):
|
||||
try:
|
||||
total_files = sum([len(files) for root, dirs, files in os.walk(src)])
|
||||
progress = 0
|
||||
|
||||
def copy_progress(src, dst):
|
||||
nonlocal progress
|
||||
shutil.copy2(src, dst)
|
||||
progress += 1
|
||||
top.after(0, progress_text.configure(text=f"Copying files: {progress}/{total_files}"))
|
||||
value = (progress / total_files) * 100
|
||||
valuep = value / 100
|
||||
progress_bar.set(valuep)
|
||||
|
||||
try:
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True, copy_function=copy_progress)
|
||||
except Exception as E:
|
||||
show_message("Error", f"Error copying files: {E}", icon="cancel")
|
||||
finally:
|
||||
top.after(0, progress_text.configure(text="0%"))
|
||||
top.after(0, progress_bar.set(0.0))
|
||||
|
||||
def check_status(var, op_var):
|
||||
if var.get():
|
||||
op_var.set(False)
|
||||
if cut_var.get():
|
||||
copy_button.configure(text=f"Start (Cut)")
|
||||
if copy_var.get():
|
||||
copy_button.configure(text=f"Start (Copy)")
|
||||
|
||||
def open_BOIII_browser():
|
||||
selected_folder = ctk.filedialog.askdirectory(title="Select boiii Folder")
|
||||
if selected_folder:
|
||||
boiii_folder_entry.delete(0, "end")
|
||||
boiii_folder_entry.insert(0, selected_folder)
|
||||
|
||||
def open_steam_browser():
|
||||
selected_folder = ctk.filedialog.askdirectory(title="Select Steam Folder (ex: C:\Program Files (x86)\Steam)")
|
||||
if selected_folder:
|
||||
steam_folder_entry.delete(0, "end")
|
||||
steam_folder_entry.insert(0, selected_folder)
|
||||
save_config("steam_folder" ,steam_folder_entry.get())
|
||||
|
||||
def start_copy_operation():
|
||||
def start_thread():
|
||||
try:
|
||||
if not cut_var.get() and not copy_var.get():
|
||||
show_message("Choose operation!", "Please choose an operation, Copy or Cut files from steam!")
|
||||
return
|
||||
|
||||
copy_button.configure(state="disabled")
|
||||
steam_folder = steam_folder_entry.get()
|
||||
ws_folder = os.path.join(steam_folder, "steamapps/workshop/content/311210")
|
||||
boiii_folder = boiii_folder_entry.get()
|
||||
|
||||
if not os.path.exists(steam_folder) and not os.path.exists(ws_folder):
|
||||
show_message("Not found", "Either you have no items downloaded from Steam or wrong path, please recheck path (ex: C:\Program Files (x86)\Steam)")
|
||||
return
|
||||
|
||||
if not os.path.exists(boiii_folder):
|
||||
show_message("Not found", "boiii folder not found, please recheck path")
|
||||
return
|
||||
|
||||
top.after(0, progress_text.configure(text="Loading..."))
|
||||
|
||||
map_folder = os.path.join(ws_folder)
|
||||
|
||||
subfolders = [f for f in os.listdir(map_folder) if os.path.isdir(os.path.join(map_folder, f))]
|
||||
total_folders = len(subfolders)
|
||||
|
||||
if not subfolders:
|
||||
show_message("No items found", f"No items found in \n{map_folder}")
|
||||
return
|
||||
|
||||
for i, workshop_id in enumerate(subfolders, start=1):
|
||||
json_file_path = os.path.join(map_folder, workshop_id, "workshop.json")
|
||||
copy_button.configure(text=f"Working on -> {i}/{total_folders}")
|
||||
|
||||
if os.path.exists(json_file_path):
|
||||
mod_type = extract_json_data(json_file_path, "Type")
|
||||
items_file = os.path.join(cwd(), LIBRARY_FILE)
|
||||
|
||||
if shared.app.library_tab.item_exists_in_file(items_file, workshop_id):
|
||||
get_folder_name = shared.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, shared.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, shared.app.settings_tab.folder_options.get())
|
||||
except:
|
||||
folder_name = extract_json_data(json_file_path, "publisherID")
|
||||
|
||||
if mod_type == "mod":
|
||||
mods_folder = os.path.join(boiii_folder, "mods")
|
||||
folder_name_path = os.path.join(mods_folder, folder_name, "zone")
|
||||
elif mod_type == "map":
|
||||
usermaps_folder = os.path.join(boiii_folder, "usermaps")
|
||||
folder_name_path = os.path.join(usermaps_folder, folder_name, "zone")
|
||||
else:
|
||||
show_message("Error", "Invalid workshop type in workshop.json, are you sure this is a map or a mod?.", icon="cancel")
|
||||
continue
|
||||
|
||||
os.makedirs(folder_name_path, exist_ok=True)
|
||||
|
||||
try:
|
||||
copy_with_progress(os.path.join(map_folder, workshop_id), folder_name_path)
|
||||
except Exception as E:
|
||||
show_message("Error", f"Error copying files: {E}", icon="cancel")
|
||||
continue
|
||||
|
||||
if cut_var.get():
|
||||
remove_tree(os.path.join(map_folder, workshop_id))
|
||||
|
||||
if subfolders:
|
||||
shared.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, shared.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))
|
||||
shared.app.create_context_menu(steam_folder_entry)
|
||||
shared.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")
|
||||
|
||||
shared.app.after(0, main_thread)
|
||||
|
3
boiiiwd_package/src/shared_vars.py
Normal file
@ -0,0 +1,3 @@
|
||||
import src.main as main
|
||||
|
||||
app = main.BOIIIWD()
|
@ -1,6 +1,5 @@
|
||||
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 *
|
||||
|
||||
class UpdateWindow(ctk.CTkToplevel):
|
||||
def __init__(self, master, update_url):
|
||||
|
13
build.py
@ -4,8 +4,8 @@ from distutils.sysconfig import get_python_lib
|
||||
site_packages_path = get_python_lib()
|
||||
|
||||
NAME = "BOIIIWD"
|
||||
SCRIPT = "boiiiwd.py"
|
||||
ICON = "boiiiwd_package/ryuk.ico"
|
||||
SCRIPT = "boiiiwd_package/boiiiwd.py"
|
||||
ICON = "boiiiwd_package/resources/ryuk.ico"
|
||||
|
||||
PyInstaller.__main__.run([
|
||||
"{}".format(SCRIPT),
|
||||
@ -15,7 +15,14 @@ PyInstaller.__main__.run([
|
||||
"--windowed",
|
||||
"--ascii",
|
||||
"--icon", f"{ICON}",
|
||||
"--add-data", "boiiiwd_package/src/resources;resources",
|
||||
"--add-data", "boiiiwd_package/resources;resources",
|
||||
"--add-data", "boiiiwd_package/src;imports",
|
||||
"--add-data", "boiiiwd_package/src;helpers",
|
||||
"--add-data", "boiiiwd_package/src;shared_vars",
|
||||
"--add-data", "boiiiwd_package/src;library_tab",
|
||||
"--add-data", "boiiiwd_package/src;settings_tab",
|
||||
"--add-data", "boiiiwd_package/src;update_window",
|
||||
"--add-data", "boiiiwd_package/src;main",
|
||||
"--add-data", f"{site_packages_path}\customtkinter;customtkinter",
|
||||
"--add-data", f"{site_packages_path}\CTkMessagebox;CTkMessagebox",
|
||||
"--add-data", f"{site_packages_path}\CTkToolTip;CTkToolTip",
|
||||
|