from src.imports import * 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() self.no_items_label = ctk.CTkLabel(self, text="", anchor="w") self.filter_entry = ctk.CTkEntry(self, placeholder_text="Your search query here, or type in mod or map to only see that") self.filter_entry.bind("", self.filter_items) self.filter_entry.grid(row=0, column=0, padx=(10, 20), pady=(10, 20), sticky="we") filter_refresh_button_image = os.path.join(RESOURCES_DIR, "Refresh_icon.svg.png") update_button_image = os.path.join(RESOURCES_DIR, "update_icon.png") self.filter_refresh_button = ctk.CTkButton(self, image=ctk.CTkImage(Image.open(filter_refresh_button_image)), command=self.refresh_items, width=20, height=20, fg_color="transparent", text="") self.filter_refresh_button.grid(row=0, column=1, padx=(10, 0), pady=(10, 20), sticky="nw") self.update_button = ctk.CTkButton(self, image=ctk.CTkImage(Image.open(update_button_image)), command=self.check_for_updates, width=65, height=20, text="", fg_color="transparent") self.update_button.grid(row=0, column=1, padx=(0, 20), pady=(10, 20), sticky="en") self.update_tooltip = CTkToolTip(self.update_button, message="Check items for updates", topmost=True) filter_tooltip = CTkToolTip(self.filter_refresh_button, message="Refresh library", topmost=True) self.label_list = [] self.button_list = [] self.button_view_list = [] self.file_cleaned = False self.filter_type = True self.clipboard_has_content = False self.item_block_list = set() self.added_folders = set() self.ids_added = set() def add_item(self, item, image=None, workshop_id=None, folder=None, invalid_warn=False): label = ctk.CTkLabel(self, text=item, image=image, compound="left", padx=5, anchor="w") button = ctk.CTkButton(self, text="Remove", width=60, height=24, fg_color="#3d3f42") button_view = ctk.CTkButton(self, text="Details", width=55, height=24, fg_color="#3d3f42") button.configure(command=lambda: self.remove_item(item, folder, workshop_id)) button_view.configure(command=lambda: self.show_map_info(workshop_id, 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") button.grid(row=len(self.button_list) + 1, column=1, pady=(0, 10), padx=(50, 10), sticky="e") button_view.grid(row=len(self.button_view_list) + 1, column=1, pady=(0, 10), padx=(10, 75), sticky="w") self.label_list.append(label) self.button_list.append(button) self.button_view_list.append(button_view) label.bind("", lambda event, label=label: self.on_label_hover(label, enter=True)) label.bind("", lambda event, label=label: self.on_label_hover(label, enter=False)) label.bind("", lambda event, label=label: self.copy_to_clipboard(label, workshop_id, event)) label.bind("", lambda event, label=label: self.copy_to_clipboard(label, workshop_id, event, append=True)) label.bind("", lambda event: self.open_folder_location(folder, event)) label.bind("", lambda event, label=label: self.copy_to_clipboard(label, folder, event)) if invalid_warn: label_warn = CTkToolTip(label, message="Duplicated or Blocked item (Search item id in search)") def on_label_hover(self, label, enter): if enter: label.configure(fg_color="#272727") else: label.configure(fg_color="transparent") def copy_to_clipboard(self, label, something, event=None, append=False): try: if append: if self.clipboard_has_content: label.clipboard_append(f"\n{something}") show_noti(label, "Appended to clipboard", event, 1.0) else: label.clipboard_clear() label.clipboard_append(something) self.clipboard_has_content = True show_noti(label, "Copied to clipboard", event, 1.0) else: label.clipboard_clear() label.clipboard_append(something) self.clipboard_has_content = True show_noti(label, "Copied to clipboard", event, 1.0) except: pass def open_folder_location(self, folder, event=None): if os.path.exists(folder): os.startfile(folder) show_noti(self, "Opening folder", event, 1.0) def item_exists_in_file(self, items_file, workshop_id, folder_name=None): if not os.path.exists(items_file): return False, False with open(items_file, "r") as f: items_data = json.load(f) for item_info in items_data: if "id" in item_info and "folder_name" in item_info and "json_folder_name" in item_info: if item_info["id"] == workshop_id and item_info["folder_name"] == folder_name: if item_info["folder_name"] in self.added_folders: continue if item_info["folder_name"] in self.item_block_list: return False ,None return True, True elif item_info["id"] == workshop_id: if item_info["folder_name"] in self.added_folders: continue if item_info["folder_name"] in self.item_block_list: return False ,None return True, False elif "id" in item_info and item_info["id"] == workshop_id: return True, False return False, False def remove_item_by_option(self, items_file, option, option_name="id"): if not os.path.exists(items_file): return with open(items_file, "r") as f: items_data = json.load(f) updated_items_data = [item for item in items_data if item.get(option_name) != option] if len(updated_items_data) < len(items_data): with open(items_file, "w") as f: json.dump(updated_items_data, f, indent=4) def get_item_by_id(self, items_file, item_id, return_option="all"): if not os.path.exists(items_file): return None with open(items_file, "r") as f: items_data = json.load(f) for item in items_data: if item.get("id") == item_id: if return_option == "all": return item elif return_option == return_option: return item.get(return_option) return None def get_item_index_by_id(self, items_data, item_id): for index, item in enumerate(items_data): if item.get("id") == item_id: return index return None def update_or_add_item_by_id(self, items_file, item_info, item_id): if not os.path.exists(items_file): with open(items_file, "w") as f: json.dump([item_info], f, indent=4) else: with open(items_file, "r+") as f: items_data = json.load(f) existing_item_index = self.get_item_index_by_id(items_data, item_id) if existing_item_index is not None: items_data[existing_item_index] = item_info else: items_data.append(item_info) f.seek(0) f.truncate() json.dump(items_data, f, indent=4) def clean_json_file(self, file): if not os.path.exists(file): show_message("Error", f"File '{file}' does not exist.") return with open(file, "r") as f: items_data = json.load(f) cleaned_items = [item for item in items_data if 'folder_name' in item and 'json_folder_name' in item and item['folder_name'] not in self.item_block_list and item['folder_name'] in self.added_folders] with open(file, 'w') as file: json.dump(cleaned_items, file, indent=4) def filter_items(self, event): filter_text = self.filter_entry.get().lower() for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list): item_text = label.cget("text").lower() if filter_text in item_text: label.grid() button.grid() button_view_list.grid() else: label.grid_remove() button_view_list.grid_remove() button.grid_remove() def load_items(self, boiiiFolder): maps_folder = Path(boiiiFolder) / "mods" mods_folder = Path(boiiiFolder) / "usermaps" mod_img = os.path.join(RESOURCES_DIR, "mod_image.png") map_img = os.path.join(RESOURCES_DIR, "map_image.png") b_mod_img = os.path.join(RESOURCES_DIR, "b_mod_image.png") b_map_img = os.path.join(RESOURCES_DIR, "b_map_image.png") map_count = 0 mod_count = 0 total_size = 0 folders_to_process = [mods_folder, maps_folder] items_file = os.path.join(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(): # current folder name 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()): 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 or workshop_id == "None")): try: self.remove_item_by_option(items_file, curr_folder_name, "folder_name") except: pass image_path = b_mod_img if item_type == "mod" else b_map_img text_to_add += " | ⚠️" self.added_items.add(text_to_add) if image_path is b_mod_img or image_path is b_map_img: self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), workshop_id=workshop_id, folder=zone_path.parent, invalid_warn=True) else: self.add_item(text_to_add, image=ctk.CTkImage(Image.open(image_path)), workshop_id=workshop_id, folder=zone_path.parent) id_found, folder_found = self.item_exists_in_file(items_file, workshop_id, curr_folder_name) item_info = { "id": workshop_id, "text": text_to_add, "date": date_added, "folder_name": curr_folder_name, "json_folder_name": folder_name } # when item is blocked ,item_exists_in_file() returns None for folder_found if not id_found and folder_found == None: self.remove_item_by_option(items_file, curr_folder_name, "folder_name") elif not id_found and not folder_found and curr_folder_name not in self.item_block_list and workshop_id not in self.ids_added: if not os.path.exists(items_file): with open(items_file, "w") as f: json.dump([item_info], f, indent=4) else: with open(items_file, "r+") as f: items_data = json.load(f) items_data.append(item_info) f.seek(0) json.dump(items_data, f, indent=4) if id_found and not folder_found and curr_folder_name not in self.item_block_list and workshop_id not in self.ids_added: self.update_or_add_item_by_id(items_file, item_info, workshop_id) # keep here cuz of item_exists_in_file() testing self.added_folders.add(curr_folder_name) if not workshop_id in self.ids_added: self.ids_added.add(workshop_id) if not self.file_cleaned and os.path.exists(items_file): self.file_cleaned = True self.clean_json_file(items_file) if not self.added_items: self.show_no_items_message() else: self.hide_no_items_message() if map_count > 0 or mod_count > 0: return f"Maps: {map_count} - Mods: {mod_count} - Total size: {convert_bytes_to_readable(total_size)}" return "No items in current selected folder" def update_item(self, boiiiFolder, id, item_type, foldername): try: if item_type == "map": folder_path = Path(boiiiFolder) / "usermaps" / f"{foldername}" elif item_type == "mod": folder_path = Path(boiiiFolder) / "mods" / f"{foldername}" else: raise ValueError("Unsupported item_type. It must be 'map' or 'mod'.") for zone_path in folder_path.glob("**/zone"): json_path = zone_path / "workshop.json" if json_path.exists(): workshop_id = extract_json_data(json_path, "PublisherID") if workshop_id == id: name = extract_json_data(json_path, "Title").replace(">", "").replace("^", "") name = name[:45] + "..." if len(name) > 45 else name item_type = extract_json_data(json_path, "Type") folder_name = extract_json_data(json_path, "FolderName") size = convert_bytes_to_readable(get_folder_size(zone_path.parent)) text_to_add = f"{name} | Type: {item_type.capitalize()}" mode_type = "ZM" if item_type == "map" and folder_name.startswith("zm") else "MP" if folder_name.startswith("mp") and item_type == "map" else None if mode_type: text_to_add += f" | Mode: {mode_type}" text_to_add += f" | ID: {workshop_id} | Size: {size}" date_added = datetime.now().strftime("%d %b, %Y @ %I:%M%p") items_file = os.path.join(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: show_message("Error" ,f"Error removing folder '{folder}': {e}", icon="cancel") return label.destroy() button.destroy() button_view_list.destroy() self.label_list.remove(label) self.button_list.remove(button) self.added_items.remove(label.cget("text")) self.ids_added.remove(id) self.button_view_list.remove(button_view_list) self.remove_item_by_option(items_file, id) def refresh_items(self): for label, button, button_view_list in zip(self.label_list, self.button_list, self.button_view_list): label.destroy() button.destroy() button_view_list.destroy() self.label_list.clear() self.button_list.clear() self.button_view_list.clear() self.added_items.clear() self.added_folders.clear() self.ids_added.clear() status = self.load_items(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}" webbrowser.open(url) def show_no_items_message(self): self.no_items_label.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="n") self.no_items_label.configure(text="No items found in the selected folder. \nMake sure you have a mod/map downloaded and or have the right boiii folder selected.") def hide_no_items_message(self): self.no_items_label.configure(text="") self.no_items_label.forget() @if_internet_available 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 if not workshop_id.isdigit(): try: if extract_workshop_id(workshop_id).strip().isdigit(): workshop_id = extract_workshop_id(workshop_id).strip() else: show_message("Warning", "Not a valid Workshop ID.") except: show_message("Warning", "Not a valid Workshop ID.") return try: url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}" response = requests.get(url) response.raise_for_status() content = response.text soup = BeautifulSoup(content, "html.parser") try: map_mod_type = soup.find("div", class_="rightDetailsBlock").text.strip() map_name = soup.find("div", class_="workshopItemTitle").text.strip() map_size = map_size = get_workshop_file_size(workshop_id, raw=True) details_stats_container = soup.find("div", class_="detailsStatsContainerRight") details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight") date_created = details_stat_elements[1].text.strip() try: ratings = soup.find('div', class_='numRatings') ratings_text = ratings.get_text() except: ratings = "Not found" ratings_text= "Not enough ratings" try: date_updated = details_stat_elements[2].text.strip() except: date_updated = "Not updated" stars_div = soup.find("div", class_="fileRatingDetails") starts = stars_div.find("img")["src"] except: show_message("Warning", "Not a valid Workshop ID\nCouldn't get information.") for button_view in self.button_view_list: button_view.configure(state="normal") return try: preview_image_element = soup.find("img", id="previewImage") workshop_item_image_url = preview_image_element["src"] except: try: preview_image_element = soup.find("img", id="previewImageMain") workshop_item_image_url = preview_image_element["src"] except Exception as e: show_message("Warning", f"Failed to get preview image ,probably wrong link/id if not please open an issue on github.\n{e}") for button_view in self.button_view_list: button_view.configure(state="normal") return starts_image_response = requests.get(starts) stars_image = Image.open(io.BytesIO(starts_image_response.content)) stars_image_size = stars_image.size image_response = requests.get(workshop_item_image_url) image_response.raise_for_status() image = Image.open(io.BytesIO(image_response.content)) image_size = image.size self.toplevel_info_window(map_name, map_mod_type, map_size, image, image_size, date_created, date_updated, stars_image, stars_image_size, ratings_text, url, workshop_id, invalid_warn, folder) except Exception as e: show_message("Error", f"Failed to fetch map information.\nError: {e}", icon="cancel") for button_view in self.button_view_list: button_view.configure(state="normal") return info_thread = threading.Thread(target=show_map_thread) info_thread.start() def toplevel_info_window(self, map_name, map_mod_type, map_size, image, image_size, date_created ,date_updated, stars_image, stars_image_size, ratings_text, url, workshop_id, invalid_warn, folder): 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') down_date = self.get_item_by_id(items_file, workshop_id, 'date') def close_window(): top.destroy() def view_map_mod(): webbrowser.open(url) def check_for_updates(): try: if check_item_date(down_date, date_updated): if show_message("There is an update.", "Press download to redownload!", icon="info", _return=True, option_1="No", option_2="Download"): if 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") stars_frame.columnconfigure(0, weight=0) stars_frame.rowconfigure(0, weight=1) image_frame = ctk.CTkFrame(top) image_frame.grid(row=1, column=0, columnspan=2, padx=20, pady=0, sticky="nsew") info_frame = ctk.CTkFrame(top) info_frame.grid(row=2, column=0, columnspan=2, padx=20, pady=20, sticky="nsew") buttons_frame = ctk.CTkFrame(top) buttons_frame.grid(row=3, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="nsew") # fillers name_label = ctk.CTkLabel(info_frame, text=f"Name: {map_name}") name_label.grid(row=0, column=0, columnspan=2, sticky="w", padx=20, pady=5) type_label = ctk.CTkLabel(info_frame, text=f"Type: {map_mod_type}") type_label.grid(row=1, column=0, columnspan=2, sticky="w", padx=20, pady=5) size_label = ctk.CTkLabel(info_frame, text=f"Size (Workshop): {map_size}") size_label.grid(row=2, column=0, columnspan=2, sticky="w", padx=20, pady=5) date_created_label = ctk.CTkLabel(info_frame, text=f"Posted: {date_created}") date_created_label.grid(row=3, column=0, columnspan=2, sticky="w", padx=20, pady=5) date_updated_label = ctk.CTkLabel(info_frame, text=f"Updated: {date_updated}") date_updated_label.grid(row=4, column=0, columnspan=2, sticky="w", padx=20, pady=5) date_updated_label = ctk.CTkLabel(info_frame, text=f"Downloaded at: {down_date}") date_updated_label.grid(row=5, column=0, columnspan=2, sticky="w", padx=20, pady=5) folder_name = ctk.CTkLabel(info_frame, text=f"Folder name: {os.path.basename(folder)}") folder_name.grid(row=6, column=0, columnspan=2, sticky="w", padx=20, pady=5) stars_image_label = ctk.CTkLabel(stars_frame) stars_width, stars_height = stars_image_size stars_image_widget = ctk.CTkImage(stars_image, size=(int(stars_width), int(stars_height))) stars_image_label.configure(image=stars_image_widget, text="") stars_image_label.pack(side="left", padx=(10, 20), pady=(10, 10)) ratings = ctk.CTkLabel(stars_frame) ratings.configure(text=ratings_text) ratings.pack(side="right", padx=(10, 20), pady=(10, 10)) image_label = ctk.CTkLabel(image_frame) width, height = image_size image_widget = ctk.CTkImage(image, size=(int(width), int(height))) image_label.configure(image=image_widget, text="") image_label.pack(expand=True, fill="both", padx=(10, 20), pady=(10, 10)) # Buttons close_button = ctk.CTkButton(buttons_frame, text="View", command=view_map_mod, width=130) close_button.grid(row=0, column=0, padx=(20, 20), pady=(10, 10), sticky="n") update_btn = ctk.CTkButton(buttons_frame, text="Update", command=check_for_updates, width=130) update_btn.grid(row=0, column=1, padx=(10, 20), pady=(10, 10), sticky="n") update_btn_tooltip = CTkToolTip(update_btn, message="Checks and installs updates of the current selected item (redownload!)", topmost=True) view_button = ctk.CTkButton(buttons_frame, text="Close", command=close_window, width=130) view_button.grid(row=0, column=2, padx=(10, 20), pady=(10, 10), sticky="n") if invalid_warn: update_btn.configure(text="Update", state="disabled") update_btn_tooltip.configure(message="Disabled due to item being blocked or duplicated") top.grid_rowconfigure(0, weight=0) top.grid_rowconfigure(1, weight=0) top.grid_rowconfigure(2, weight=1) top.grid_columnconfigure(0, weight=1) top.grid_columnconfigure(1, weight=1) buttons_frame.grid_rowconfigure(0, weight=1) buttons_frame.grid_rowconfigure(1, weight=1) buttons_frame.grid_rowconfigure(2, weight=1) buttons_frame.grid_columnconfigure(0, weight=1) buttons_frame.grid_columnconfigure(1, weight=1) buttons_frame.grid_columnconfigure(2, weight=1) finally: for button_view in self.button_view_list: button_view.configure(state="normal") self.after(0, main_thread) @if_internet_available def check_for_updates(self, on_launch=False): self.after(1, self.update_button.configure(state="disabled")) self.update_tooltip.configure(message='Still loading please wait...') cevent = Event() cevent.x_root = self.update_button.winfo_rootx() cevent.y_root = self.update_button.winfo_rooty() if not on_launch: show_noti(self.update_button, "Please wait, window will popup shortly", event=cevent, noti_dur=3.0, topmost=True) threading.Thread(target=self.check_items_func, args=(on_launch,)).start() def items_update_message(self, to_update_len): def main_thread(): if show_message(f"{to_update_len} Item updates available", f"{to_update_len} Workshop Items have an update, Would you like to open the item updater window?", icon="info", _return=True): main_app.app.after(1, self.update_items_window) else: return main_app.app.after(0, main_thread) self.update_button.configure(state="normal", width=65, height=20) self.update_tooltip.configure(message='Check items for updates') return def check_items_func(self, on_launch): # Needed to refresh item that needs updates self.to_update.clear() def if_id_needs_update(item_id, item_date, text): try: headers = {'Cache-Control': 'no-cache'} url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={item_id}" response = requests.get(url, headers=headers) response.raise_for_status() content = response.text soup = BeautifulSoup(content, "html.parser") details_stats_container = soup.find("div", class_="detailsStatsContainerRight") details_stat_elements = details_stats_container.find_all("div", class_="detailsStatRight") try: date_updated = details_stat_elements[2].text.strip() except: try: date_updated = details_stat_elements[1].text.strip() except: return False if check_item_date(item_date, date_updated): self.to_update.add(text + f" | Updated: {date_updated}") return True else: return False except Exception as e: show_message("Error", f"Error occured\n{e}", icon="cancel") return def check_for_update(): lib_data = None if not os.path.exists(os.path.join(application_path, LIBRARY_FILE)): show_message("Error checking for item updates! -> Setting is on", "Please visit library tab at least once with the correct boiii path!, you also need to have at lease 1 item!") return with open(LIBRARY_FILE, 'r') as file: lib_data = json.load(file) for item in lib_data: item_id = item["id"] item_date = item["date"] if_id_needs_update(item_id, item_date, item["text"]) check_for_update() to_update_len = len(self.to_update) if to_update_len > 0: self.items_update_message(to_update_len) else: self.update_button.configure(state="normal", width=65, height=20) self.update_tooltip.configure(message='Check items for updates') if not on_launch: show_message("No updates found!", "Items are up to date!", icon="info") def update_items_window(self): try: top = ctk.CTkToplevel(master=None) top.withdraw() if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")): top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico"))) top.title("Item updater - List of Items with Updates - Click to select 1 or more") longest_text_length = max(len(text) for text in self.to_update) window_width = longest_text_length * 6 + 5 top.geometry(f"{window_width}x450") top.attributes('-topmost', 'true') top.resizable(True, True) selected_id_list = [] cevent = Event() self.select_all_bool = False listbox = CTkListbox(top, multiple_selection=True) listbox.grid(row=0, column=0, sticky="nsew") update_button = ctk.CTkButton(top, text="Update") update_button.grid(row=1, column=0, pady=10, padx=5, sticky='ns') select_button = ctk.CTkButton(top, text="Select All", width=5) select_button.grid(row=1, column=0, pady=10, padx=(230, 0), sticky='ns') def open_url(id_part, e=None): url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={id_part}" webbrowser.open(url) # you gotta use my modded CTkListbox originaly by Akascape def add_checkbox_item(index, item_text): parts = item_text.split('ID: ') id_part = parts[1].split('|')[0].strip() listbox.insert(index, item_text, keybind="", func=lambda e: open_url(id_part)) def load_items(): for index, item_text in enumerate(self.to_update): if index == len(self.to_update) - 1: add_checkbox_item("end", item_text) top.deiconify() return add_checkbox_item(index, item_text) def update_list(selected_option): selected_id_list.clear() if selected_option: for option in selected_option: parts = option.split('ID: ') if len(parts) > 1: id_part = parts[1].split('|')[0].strip() selected_id_list.append(id_part) def select_all(): if self.select_all_bool: listbox.deactivate("all") update_list(listbox.get()) self.select_all_bool = False return listbox.deactivate("all") listbox.activate("all") update_list(listbox.get()) self.select_all_bool = True def update_btn_fun(): if len(selected_id_list) == 1: if main_app.app.is_downloading: show_message("Error", "Please wait for the current download to finish or stop it then start.", icon="cancel") return main_app.app.edit_workshop_id.delete(0, "end") main_app.app.edit_workshop_id.insert(0, selected_id_list[0]) main_app.app.main_button_event() main_app.app.download_map(update=True) top.destroy() return elif len(selected_id_list) > 1: if main_app.app.is_downloading: show_message("Error", "Please wait for the current download to finish or stop it then start.", icon="cancel") return comma_separated_ids = ",".join(selected_id_list) main_app.app.queuetextarea.delete("1.0", "end") main_app.app.queuetextarea.insert("1.0", comma_separated_ids) main_app.app.queue_button_event() main_app.app.download_map(update=True) top.destroy() return else: cevent.x_root = update_button.winfo_rootx() cevent.y_root = update_button.winfo_rooty() show_noti(update_button ,"Please select 1 or more items", event=cevent, noti_dur=0.8, topmost=True) listbox.configure(command=update_list) update_button.configure(command=update_btn_fun) select_button.configure(command=select_all) top.grid_rowconfigure(0, weight=1) top.grid_columnconfigure(0, weight=1) load_items() except Exception as e: show_message("Error", f"{e}", icon="cancel") finally: self.update_button.configure(state="normal", width=65, height=20) self.update_tooltip.configure(message='Check items for updates')