├── .github └── workflows │ └── build.yml ├── .gitignore ├── README.md ├── boiiiwd_package ├── boiiiwd.py ├── resources │ ├── Refresh_icon.svg.png │ ├── b_map_image.png │ ├── b_mod_image.png │ ├── boiiiwd_blue.json │ ├── boiiiwd_ghost.json │ ├── boiiiwd_grey.json │ ├── boiiiwd_neonbanana.json │ ├── boiiiwd_obsidian.json │ ├── boiiiwd_test.json │ ├── boiiiwd_theme.json │ ├── default_library_img.png │ ├── map_image.png │ ├── mod_image.png │ ├── ryuk.ico │ ├── ryuk.png │ ├── sett10.png │ └── update_icon.png └── src │ ├── CTkListbox │ ├── __init__.py │ └── ctk_listbox.py │ ├── CTkToolTip │ ├── __init__.py │ └── ctk_tooltip.py │ ├── __init__.py │ ├── helpers.py │ ├── imports.py │ ├── library_tab.py │ ├── main.py │ ├── settings_tab.py │ ├── shared_vars.py │ ├── update_window.py │ └── winpty_patch.py ├── build.py ├── md └── LOGIN.md ├── requirements.txt └── utils └── enc_key_gen.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.12.3" 23 | architecture: "x64" 24 | 25 | - name: Install Python dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -r requirements.txt 29 | 30 | - name: Extract version 31 | id: get_version 32 | run: | 33 | $version = (Get-Content boiiiwd_package/src/imports.py | Select-String -Pattern 'VERSION = "(.*)"' | ForEach-Object { $_.Matches[0].Groups[1].Value }) 34 | echo "VERSION=$version" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 35 | 36 | - name: Build executable 37 | run: | 38 | python build.py 39 | 40 | - name: Create ZIP file 41 | run: | 42 | powershell Compress-Archive -Path BOIIIWD.exe -DestinationPath Release.zip 43 | 44 | - name: Release 45 | uses: ncipollo/release-action@v1 46 | with: 47 | artifacts: "Release.zip" 48 | draft: true 49 | allowUpdates: true 50 | artifactErrorsFailBuild: true 51 | generateReleaseNotes: true 52 | token: ${{ secrets.PAT }} 53 | tag: ${{ env.VERSION }} 54 | name: ${{ env.VERSION }} 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.PAT }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # Other 163 | *.ini 164 | *.conf 165 | .vscode 166 | test/ 167 | steamcmd 168 | boiiiwd_library.json 169 | *.exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BOIIIWD 2 | - A Feature-rich GUI Steam Workshop downloader for [Call of Duty®: Black Ops III](https://store.steampowered.com/app/311210/Call_of_Duty_Black_Ops_III/) built using CustomTkinter
3 | 4 | 5 | 6 | 9 | 12 | 15 | 16 |
7 | 8 | 10 | 11 | 13 | 14 |
17 | 18 | ## Usage: 19 | - Run [BOIIIWD.exe](https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip) ([VirusTotal Scan](https://www.virustotal.com/gui/file/5ca1367a82893a1f412b59a52431e9ac4219a67a50c294ee86a7d41473826b14/detection)) 20 | - [Optional] Run as script:```python boiiiwd_package\boiiiwd.py``` 21 | 22 | ## Features: 23 | - Improved download stability 24 | - Auto installs mods and maps 25 | - Queue -> download items in queue 26 | - Library tab -> lists your downloaded items 27 | - Item updater -> checks your items for updates 28 | - Workshop Transfer -> copy/move items from the workshop folder into the game directory 29 | - Custom Themes 30 | 31 | ## Notes: 32 | - Steamcmd will be downloaded if it is not installed
33 | - Initializing SteamCMD for the first time could take some time depending on your internet speed
34 | 35 | #### Mouse Bindings: 36 | Library Tab: 37 | 38 | * Mouse1 -> copy id 39 | * Ctrl + Mouse1 -> append to clipboard 40 | * Mouse2 (scroll wheel button) -> open item path in file explorer 41 | * Mouse3 (Right click) -> copy path 42 | 43 | ## Building from Source: 44 | - ```pip install -r requirements.txt``` -> use my modified [CTkToolTip](./CTkToolTip) and [CTkListbox](./CTkListbox) 45 | - ```python build.py``` 46 | 47 | ## Login and Download Stability: 48 | Logging in can significantly improve download stability, as reported by some users online. If you're having trouble figuring out how to log in, follow the detailed tutorial provided in the [LOGIN.md](./md/LOGIN.md) file. 49 | 50 | --- 51 | 52 | **Disclaimer:** You can change/add the environment variable `BOIIIWD_ENC_KEY` used for encrypting your steam username to whatever you want. You can use [this helper function](./utils/enc_key_gen.py) to generate a valid key for you. -------------------------------------------------------------------------------- /boiiiwd_package/boiiiwd.py: -------------------------------------------------------------------------------- 1 | import src.shared_vars as main_app 2 | 3 | if __name__ == "__main__": 4 | main_app.app.mainloop() 5 | -------------------------------------------------------------------------------- /boiiiwd_package/resources/Refresh_icon.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/Refresh_icon.svg.png -------------------------------------------------------------------------------- /boiiiwd_package/resources/b_map_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/b_map_image.png -------------------------------------------------------------------------------- /boiiiwd_package/resources/b_mod_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/b_mod_image.png -------------------------------------------------------------------------------- /boiiiwd_package/resources/boiiiwd_blue.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOIIIWD_Globals": { 3 | "button_active_state_color": "#0e2540", 4 | "button_normal_state_color": "#1f538d", 5 | "progress_bar_fill_color": "#1f538d", 6 | "this_is_a_comment": "For button hover color check CTkButton bellow + other stuff" 7 | }, 8 | "CTk": { 9 | "fg_color": ["gray95", "gray10"] 10 | }, 11 | "CTkToplevel": { 12 | "fg_color": ["gray95", "gray10"] 13 | }, 14 | "CTkFrame": { 15 | "corner_radius": 6, 16 | "border_width": 0, 17 | "fg_color": ["gray90", "gray13"], 18 | "top_fg_color": ["gray85", "gray16"], 19 | "border_color": ["gray65", "gray28"] 20 | }, 21 | "CTkButton": { 22 | "corner_radius": 6, 23 | "border_width": 0, 24 | "fg_color": ["#3a7ebf", "#1f538d"], 25 | "hover_color": ["#325882", "#14375e"], 26 | "border_color": ["#3E454A", "#949A9F"], 27 | "text_color": ["#DCE4EE", "#DCE4EE"], 28 | "text_color_disabled": ["gray74", "gray60"] 29 | }, 30 | "CTkLabel": { 31 | "corner_radius": 0, 32 | "fg_color": "transparent", 33 | "text_color": ["gray14", "gray84"] 34 | }, 35 | "CTkEntry": { 36 | "corner_radius": 6, 37 | "border_width": 2, 38 | "fg_color": ["#F9F9FA", "#343638"], 39 | "border_color": ["#979DA2", "#565B5E"], 40 | "text_color": ["gray14", "gray84"], 41 | "placeholder_text_color": ["gray52", "gray62"] 42 | }, 43 | "CTkCheckBox": { 44 | "corner_radius": 6, 45 | "border_width": 3, 46 | "fg_color": ["#3a7ebf", "#1f538d"], 47 | "border_color": ["#3E454A", "#949A9F"], 48 | "hover_color": ["#325882", "#14375e"], 49 | "checkmark_color": ["#DCE4EE", "gray90"], 50 | "text_color": ["gray14", "gray84"], 51 | "text_color_disabled": ["gray60", "gray45"] 52 | }, 53 | "CTkSwitch": { 54 | "corner_radius": 1000, 55 | "border_width": 3, 56 | "button_length": 0, 57 | "fg_color": ["#939BA2", "#4A4D50"], 58 | "progress_color": ["#3a7ebf", "#1f538d"], 59 | "button_color": ["gray36", "#D5D9DE"], 60 | "button_hover_color": ["gray20", "gray100"], 61 | "text_color": ["gray14", "gray84"], 62 | "text_color_disabled": ["gray60", "gray45"] 63 | }, 64 | "CTkRadioButton": { 65 | "corner_radius": 1000, 66 | "border_width_checked": 6, 67 | "border_width_unchecked": 3, 68 | "fg_color": ["#3a7ebf", "#1f538d"], 69 | "border_color": ["#3E454A", "#949A9F"], 70 | "hover_color": ["#325882", "#14375e"], 71 | "text_color": ["gray14", "gray84"], 72 | "text_color_disabled": ["gray60", "gray45"] 73 | }, 74 | "CTkProgressBar": { 75 | "corner_radius": 1000, 76 | "border_width": 0, 77 | "fg_color": ["#939BA2", "#4A4D50"], 78 | "progress_color": ["#3a7ebf", "#1f538d"], 79 | "border_color": ["gray", "gray"] 80 | }, 81 | "CTkSlider": { 82 | "corner_radius": 1000, 83 | "button_corner_radius": 1000, 84 | "border_width": 6, 85 | "button_length": 0, 86 | "fg_color": ["#939BA2", "#4A4D50"], 87 | "progress_color": ["gray40", "#AAB0B5"], 88 | "button_color": ["#3a7ebf", "#1f538d"], 89 | "button_hover_color": ["#325882", "#14375e"] 90 | }, 91 | "CTkOptionMenu": { 92 | "corner_radius": 6, 93 | "fg_color": ["#3a7ebf", "#1f538d"], 94 | "button_color": ["#325882", "#14375e"], 95 | "button_hover_color": ["#234567", "#1e2c40"], 96 | "text_color": ["#DCE4EE", "#DCE4EE"], 97 | "text_color_disabled": ["gray74", "gray60"] 98 | }, 99 | "CTkComboBox": { 100 | "corner_radius": 6, 101 | "border_width": 2, 102 | "fg_color": ["#F9F9FA", "#343638"], 103 | "border_color": ["#979DA2", "#565B5E"], 104 | "button_color": ["#979DA2", "#565B5E"], 105 | "button_hover_color": ["#6E7174", "#7A848D"], 106 | "text_color": ["gray14", "gray84"], 107 | "text_color_disabled": ["gray50", "gray45"] 108 | }, 109 | "CTkScrollbar": { 110 | "corner_radius": 1000, 111 | "border_spacing": 4, 112 | "fg_color": "transparent", 113 | "button_color": ["gray55", "gray41"], 114 | "button_hover_color": ["gray40", "gray53"] 115 | }, 116 | "CTkSegmentedButton": { 117 | "corner_radius": 6, 118 | "border_width": 2, 119 | "fg_color": ["#979DA2", "gray29"], 120 | "selected_color": ["#3a7ebf", "#1f538d"], 121 | "selected_hover_color": ["#325882", "#14375e"], 122 | "unselected_color": ["#979DA2", "gray29"], 123 | "unselected_hover_color": ["gray70", "gray41"], 124 | "text_color": ["#DCE4EE", "#DCE4EE"], 125 | "text_color_disabled": ["gray74", "gray60"] 126 | }, 127 | "CTkTextbox": { 128 | "corner_radius": 6, 129 | "border_width": 0, 130 | "fg_color": ["gray100", "gray20"], 131 | "border_color": ["#979DA2", "#565B5E"], 132 | "text_color": ["gray14", "gray84"], 133 | "scrollbar_button_color": ["gray55", "gray41"], 134 | "scrollbar_button_hover_color": ["gray40", "gray53"] 135 | }, 136 | "CTkScrollableFrame": { 137 | "label_fg_color": ["gray80", "gray21"] 138 | }, 139 | "DropdownMenu": { 140 | "fg_color": ["gray90", "gray20"], 141 | "hover_color": ["gray75", "gray28"], 142 | "text_color": ["gray14", "gray84"] 143 | }, 144 | "CTkFont": { 145 | "macOS": { 146 | "family": "SF Display", 147 | "size": 13, 148 | "weight": "normal" 149 | }, 150 | "Windows": { 151 | "family": "Roboto", 152 | "size": 13, 153 | "weight": "normal" 154 | }, 155 | "Linux": { 156 | "family": "Roboto", 157 | "size": 13, 158 | "weight": "normal" 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /boiiiwd_package/resources/boiiiwd_ghost.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOIIIWD_Globals": { 3 | "button_active_state_color": "#a9b8c4", 4 | "button_normal_state_color": "#11202b", 5 | "progress_bar_fill_color": "#a9b8c4", 6 | "this_is_a_comment": "For button hover color check CTkButton bellow + other stuff", 7 | "credits": "originally by avalon60" 8 | }, 9 | "CTk": { 10 | "fg_color": [ 11 | "#c6ced8", 12 | "#c6ced8" 13 | ] 14 | }, 15 | "CTkToplevel": { 16 | "fg_color": [ 17 | "#c6ced8", 18 | "#c6ced8" 19 | ] 20 | }, 21 | "CTkFrame": { 22 | "corner_radius": 6, 23 | "border_width": 0, 24 | "fg_color": [ 25 | "#d3dbe1", 26 | "#d3dbe1" 27 | ], 28 | "top_fg_color": [ 29 | "#cbd3d9", 30 | "#cbd3d9" 31 | ], 32 | "border_color": [ 33 | "#586b78", 34 | "#586b78" 35 | ] 36 | }, 37 | "CTkButton": { 38 | "corner_radius": 6, 39 | "border_width": 0, 40 | "fg_color": [ 41 | "#697b88", 42 | "#11202b" 43 | ], 44 | "hover_color": [ 45 | "#788a97", 46 | "#000000" 47 | ], 48 | "border_color": [ 49 | "#405366", 50 | "#405366" 51 | ], 52 | "text_color": [ 53 | "#ffffff", 54 | "#ffffff" 55 | ], 56 | "text_color_disabled": [ 57 | "#c9dae7", 58 | "#97a8b5" 59 | ] 60 | }, 61 | "CTkLabel": { 62 | "corner_radius": 0, 63 | "fg_color": "transparent", 64 | "text_color": [ 65 | "#2b3449", 66 | "#2b3449" 67 | ], 68 | "text_color_disabled": [ 69 | "#2b3449", 70 | "#2b3449" 71 | ] 72 | }, 73 | "CTkEntry": { 74 | "corner_radius": 6, 75 | "border_width": 2, 76 | "fg_color": [ 77 | "#a9b8c4", 78 | "#a9b8c4" 79 | ], 80 | "border_color": [ 81 | "#464f64", 82 | "#464f64" 83 | ], 84 | "text_color": [ 85 | "#2b3449", 86 | "#2b3449" 87 | ], 88 | "placeholder_text_color": [ 89 | "#464e56", 90 | "#464e56" 91 | ] 92 | }, 93 | "CTkCheckBox": { 94 | "corner_radius": 6, 95 | "border_width": 3, 96 | "fg_color": [ 97 | "#748498", 98 | "#748498" 99 | ], 100 | "border_color": [ 101 | "#3e4654", 102 | "#3e4654" 103 | ], 104 | "hover_color": [ 105 | "#697b88", 106 | "#697b88" 107 | ], 108 | "checkmark_color": [ 109 | "#ffffff", 110 | "#ffffff" 111 | ], 112 | "text_color": [ 113 | "#2b3449", 114 | "#2b3449" 115 | ], 116 | "text_color_disabled": [ 117 | "#5e6b7c", 118 | "#5e6b7c" 119 | ] 120 | }, 121 | "CTkSwitch": { 122 | "corner_radius": 1000, 123 | "border_width": 3, 124 | "button_length": 0, 125 | "progress_color": [ 126 | "#748498", 127 | "#748498" 128 | ], 129 | "button_color": [ 130 | "#11202b", 131 | "#11202b" 132 | ], 133 | "fg_color": [ 134 | "#383e3e", 135 | "#383e3e" 136 | ], 137 | "button_hover_color": [ 138 | "#1b2631", 139 | "#1b2631" 140 | ], 141 | "text_color": [ 142 | "#2b3449", 143 | "#2b3449" 144 | ], 145 | "text_color_disabled": [ 146 | "#5e6b7c", 147 | "#5e6b7c" 148 | ] 149 | }, 150 | "CTkRadioButton": { 151 | "corner_radius": 1000, 152 | "border_width_checked": 6, 153 | "border_width_unchecked": 3, 154 | "fg_color": [ 155 | "#11202b", 156 | "#11202b" 157 | ], 158 | "border_color": [ 159 | "#303940", 160 | "#303940" 161 | ], 162 | "hover_color": [ 163 | "#5c6b78", 164 | "#5c6b78" 165 | ], 166 | "text_color": [ 167 | "#1b2631", 168 | "#1b2631" 169 | ], 170 | "text_color_disabled": [ 171 | "#5b6879", 172 | "#5b6879" 173 | ] 174 | }, 175 | "CTkProgressBar": { 176 | "corner_radius": 1000, 177 | "border_width": 0, 178 | "fg_color": [ 179 | "#383e3e", 180 | "#383e3e" 181 | ], 182 | "progress_color": [ 183 | "#748498", 184 | "#748498" 185 | ], 186 | "border_color": [ 187 | "#000000", 188 | "#000000" 189 | ] 190 | }, 191 | "CTkSlider": { 192 | "corner_radius": 1000, 193 | "button_corner_radius": 1000, 194 | "border_width": 6, 195 | "button_length": 0, 196 | "fg_color": [ 197 | "#383e3e", 198 | "#383e3e" 199 | ], 200 | "progress_color": [ 201 | "#748498", 202 | "#748498" 203 | ], 204 | "button_color": [ 205 | "#11202b", 206 | "#11202b" 207 | ], 208 | "button_hover_color": [ 209 | "#5c6b78", 210 | "#5c6b78" 211 | ] 212 | }, 213 | "CTkOptionMenu": { 214 | "corner_radius": 6, 215 | "fg_color": [ 216 | "#748498", 217 | "#748498" 218 | ], 219 | "button_color": [ 220 | "#3c5163", 221 | "#3c5163" 222 | ], 223 | "button_hover_color": [ 224 | "#183146", 225 | "#183146" 226 | ], 227 | "text_color": [ 228 | "#ffffff", 229 | "#ffffff" 230 | ], 231 | "text_color_disabled": [ 232 | "#c0c2c4", 233 | "#cfd1d3" 234 | ] 235 | }, 236 | "CTkComboBox": { 237 | "corner_radius": 6, 238 | "border_width": 2, 239 | "fg_color": [ 240 | "#9dacb8", 241 | "#97a6b2" 242 | ], 243 | "border_color": [ 244 | "#464f64", 245 | "#464f64" 246 | ], 247 | "button_color": [ 248 | "#1e2e3c", 249 | "#1e2e3c" 250 | ], 251 | "button_hover_color": [ 252 | "#000000", 253 | "#000000" 254 | ], 255 | "text_color": [ 256 | "#ffffff", 257 | "#ffffff" 258 | ], 259 | "text_color_disabled": [ 260 | "#dcdee0", 261 | "#d7d9db" 262 | ] 263 | }, 264 | "CTkScrollbar": { 265 | "corner_radius": 1000, 266 | "border_spacing": 4, 267 | "fg_color": "transparent", 268 | "button_color": [ 269 | "#081722", 270 | "#081722" 271 | ], 272 | "button_hover_color": [ 273 | "#35444f", 274 | "#374350" 275 | ] 276 | }, 277 | "CTkSegmentedButton": { 278 | "corner_radius": 6, 279 | "border_width": 2, 280 | "fg_color": [ 281 | "#748498", 282 | "#748498" 283 | ], 284 | "selected_color": [ 285 | "#c3cbd5", 286 | "#c3cbd5" 287 | ], 288 | "selected_hover_color": [ 289 | "#d3dbe1", 290 | "#d3dbe1" 291 | ], 292 | "unselected_color": [ 293 | "#667885", 294 | "#667885" 295 | ], 296 | "unselected_hover_color": [ 297 | "#586b78", 298 | "#586b78" 299 | ], 300 | "text_color": [ 301 | "#1c253a", 302 | "#2b3449" 303 | ], 304 | "text_color_disabled": [ 305 | "#3a4454", 306 | "#495363" 307 | ] 308 | }, 309 | "CTkTextbox": { 310 | "corner_radius": 6, 311 | "border_width": 0, 312 | "fg_color": [ 313 | "#a9b8c4", 314 | "#a9b8c4" 315 | ], 316 | "border_color": [ 317 | "#000000", 318 | "#000000" 319 | ], 320 | "text_color": [ 321 | "#2b3449", 322 | "#2b3449" 323 | ], 324 | "scrollbar_button_color": [ 325 | "#11202b", 326 | "#11202b" 327 | ], 328 | "scrollbar_button_hover_color": [ 329 | "#2c3b46", 330 | "#404c59" 331 | ] 332 | }, 333 | "CTkScrollableFrame": { 334 | "label_fg_color": [ 335 | "#d3dbe1", 336 | "#d3dbe1" 337 | ] 338 | }, 339 | "DropdownMenu": { 340 | "fg_color": [ 341 | "#748498", 342 | "#748498" 343 | ], 344 | "hover_color": [ 345 | "#d3dbe1", 346 | "#d3dbe1" 347 | ], 348 | "text_color": [ 349 | "#000000", 350 | "#000000" 351 | ] 352 | }, 353 | "CTkFont": { 354 | "macOS": { 355 | "family": "SF Display", 356 | "size": 13, 357 | "weight": "normal" 358 | }, 359 | "Windows": { 360 | "family": "Roboto", 361 | "size": 13, 362 | "weight": "normal" 363 | }, 364 | "Linux": { 365 | "family": "Roboto", 366 | "size": 13, 367 | "weight": "normal" 368 | } 369 | }, 370 | "provenance": { 371 | "theme author": "Clive Bostock", 372 | "date created": "Mar 27 2023 18:16:02", 373 | "last modified by": "Clive Bostock", 374 | "created with": "CTk Theme Builder", 375 | "last modified": "Aug 13 2023 13:42:44", 376 | "keystone colour": "#3c5064", 377 | "harmony method": "Split-complementary", 378 | "harmony differential": null, 379 | "theme name": "GreyGhost" 380 | } 381 | } -------------------------------------------------------------------------------- /boiiiwd_package/resources/boiiiwd_grey.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOIIIWD_Globals": { 3 | "button_active_state_color": "#28292b", 4 | "button_normal_state_color": "#3e3f42", 5 | "progress_bar_fill_color": "#616368", 6 | "this_is_a_comment": "For button hover color check CTkButton bellow + other stuff" 7 | }, 8 | "CTk": { 9 | "fg_color": ["gray95", "gray10"] 10 | }, 11 | "CTkToplevel": { 12 | "fg_color": ["gray95", "gray10"] 13 | }, 14 | "CTkFrame": { 15 | "corner_radius": 6, 16 | "border_width": 0, 17 | "fg_color": ["gray90", "gray13"], 18 | "top_fg_color": ["gray85", "gray16"], 19 | "border_color": ["gray65", "gray28"] 20 | }, 21 | "CTkButton": { 22 | "corner_radius": 6, 23 | "border_width": 0, 24 | "fg_color": ["#3e3f42", "#3e3f42"], 25 | "hover_color": ["#505256", "#505256"], 26 | "border_color": ["#3E454A", "#949A9F"], 27 | "text_color": ["#fcfcfc", "#fcfcfc"], 28 | "text_color_disabled": ["gray74", "gray60"] 29 | }, 30 | "CTkLabel": { 31 | "corner_radius": 0, 32 | "fg_color": "transparent", 33 | "text_color": ["gray14", "gray84"] 34 | }, 35 | "CTkEntry": { 36 | "corner_radius": 6, 37 | "border_width": 2, 38 | "fg_color": ["#F9F9FA", "#343638"], 39 | "border_color": ["#979DA2", "#565B5E"], 40 | "text_color": ["gray14", "gray84"], 41 | "placeholder_text_color": ["gray52", "gray62"] 42 | }, 43 | "CTkCheckBox": { 44 | "corner_radius": 6, 45 | "border_width": 3, 46 | "fg_color": ["#3a3a3a", "#3a3a3a"], 47 | "hover_color": ["#737373", "#737373"], 48 | "border_color": ["#3E454A", "#949A9F"], 49 | "checkmark_color": ["#DCE4EE", "gray90"], 50 | "text_color": ["gray14", "gray84"], 51 | "text_color_disabled": ["gray60", "gray45"] 52 | }, 53 | "CTkSwitch": { 54 | "corner_radius": 1000, 55 | "border_width": 3, 56 | "button_length": 0, 57 | "fg_color": ["#3a3a3a", "#3a3a3a"], 58 | "progress_color": ["green", "green"], 59 | "button_color": ["gray36", "#D5D9DE"], 60 | "button_hover_color": ["gray20", "gray100"], 61 | "text_color": ["gray14", "gray84"], 62 | "text_color_disabled": ["gray60", "gray45"] 63 | }, 64 | "CTkRadioButton": { 65 | "corner_radius": 1000, 66 | "border_width_checked": 6, 67 | "border_width_unchecked": 3, 68 | "fg_color": ["#3a3a3a", "#3a3a3a"], 69 | "border_color": ["#3E454A", "#949A9F"], 70 | "hover_color": ["#325882", "#14375e"], 71 | "text_color": ["gray14", "gray84"], 72 | "text_color_disabled": ["gray60", "gray45"] 73 | }, 74 | "CTkProgressBar": { 75 | "corner_radius": 1000, 76 | "border_width": 0, 77 | "fg_color": ["#3a3a3a", "#3a3a3a"], 78 | "progress_color": ["#3a7ebf", "#1f538d"], 79 | "border_color": ["gray", "gray"] 80 | }, 81 | "CTkSlider": { 82 | "corner_radius": 1000, 83 | "button_corner_radius": 1000, 84 | "border_width": 6, 85 | "button_length": 0, 86 | "fg_color": ["#939BA2", "#4A4D50"], 87 | "progress_color": ["gray40", "#AAB0B5"], 88 | "button_color": ["#3a7ebf", "#1f538d"], 89 | "button_hover_color": ["#325882", "#14375e"] 90 | }, 91 | "CTkOptionMenu": { 92 | "corner_radius": 6, 93 | "fg_color": ["#505256", "#505256"], 94 | "button_color": ["#3e3f42", "#3e3f42"], 95 | "button_hover_color": ["#343638", "#343638"], 96 | "text_color": ["#DCE4EE", "#DCE4EE"], 97 | "text_color_disabled": ["gray74", "gray60"] 98 | }, 99 | "CTkComboBox": { 100 | "corner_radius": 6, 101 | "border_width": 2, 102 | "fg_color": ["#F9F9FA", "#343638"], 103 | "border_color": ["#979DA2", "#565B5E"], 104 | "button_color": ["#979DA2", "#565B5E"], 105 | "button_hover_color": ["#6E7174", "#7A848D"], 106 | "text_color": ["gray14", "gray84"], 107 | "text_color_disabled": ["gray50", "gray45"] 108 | }, 109 | "CTkScrollbar": { 110 | "corner_radius": 1000, 111 | "border_spacing": 4, 112 | "fg_color": "transparent", 113 | "button_color": ["gray55", "gray41"], 114 | "button_hover_color": ["gray40", "gray53"] 115 | }, 116 | "CTkSegmentedButton": { 117 | "corner_radius": 6, 118 | "border_width": 2, 119 | "fg_color": ["#979DA2", "gray29"], 120 | "selected_color": ["#3a7ebf", "#1f538d"], 121 | "selected_hover_color": ["#325882", "#14375e"], 122 | "unselected_color": ["#979DA2", "gray29"], 123 | "unselected_hover_color": ["gray70", "gray41"], 124 | "text_color": ["#DCE4EE", "#DCE4EE"], 125 | "text_color_disabled": ["gray74", "gray60"] 126 | }, 127 | "CTkTextbox": { 128 | "corner_radius": 6, 129 | "border_width": 0, 130 | "fg_color": ["gray100", "gray20"], 131 | "border_color": ["#979DA2", "#565B5E"], 132 | "text_color": ["gray14", "gray84"], 133 | "scrollbar_button_color": ["gray55", "gray41"], 134 | "scrollbar_button_hover_color": ["gray40", "gray53"] 135 | }, 136 | "CTkScrollableFrame": { 137 | "label_fg_color": ["gray80", "gray21"] 138 | }, 139 | "DropdownMenu": { 140 | "fg_color": ["gray90", "gray20"], 141 | "hover_color": ["gray75", "gray28"], 142 | "text_color": ["gray14", "gray84"] 143 | }, 144 | "CTkFont": { 145 | "macOS": { 146 | "family": "SF Display", 147 | "size": 13, 148 | "weight": "normal" 149 | }, 150 | "Windows": { 151 | "family": "Roboto", 152 | "size": 13, 153 | "weight": "normal" 154 | }, 155 | "Linux": { 156 | "family": "Roboto", 157 | "size": 13, 158 | "weight": "normal" 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /boiiiwd_package/resources/boiiiwd_neonbanana.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOIIIWD_Globals": { 3 | "button_active_state_color": "#200c00", 4 | "button_normal_state_color": "#532000", 5 | "progress_bar_fill_color": "#814007", 6 | "this_is_a_comment": "For button hover color check CTkButton bellow + other stuff", 7 | "credits": "originally by avalon60" 8 | }, 9 | "CTk": { 10 | "fg_color": [ 11 | "#0a0a0a", 12 | "#0a0a0a" 13 | ] 14 | }, 15 | "CTkToplevel": { 16 | "fg_color": [ 17 | "#0f0f0f", 18 | "#0f0f0f" 19 | ] 20 | }, 21 | "CTkFrame": { 22 | "corner_radius": 6, 23 | "border_width": 1, 24 | "fg_color": [ 25 | "#261409", 26 | "#261409" 27 | ], 28 | "top_fg_color": [ 29 | "#190a0a", 30 | "#190a0a" 31 | ], 32 | "border_color": [ 33 | "#f4cb29", 34 | "#f4cb29" 35 | ] 36 | }, 37 | "CTkButton": { 38 | "corner_radius": 6, 39 | "border_width": 2, 40 | "fg_color": [ 41 | "#532000", 42 | "#532000" 43 | ], 44 | "hover_color": [ 45 | "#814007", 46 | "#814007" 47 | ], 48 | "border_color": [ 49 | "#f4cb29", 50 | "#f4cb29" 51 | ], 52 | "text_color": [ 53 | "#f4cb29", 54 | "#f4cb29" 55 | ], 56 | "text_color_disabled": [ 57 | "#98591f", 58 | "#98591f" 59 | ] 60 | }, 61 | "CTkLabel": { 62 | "corner_radius": 0, 63 | "fg_color": "transparent", 64 | "text_color": [ 65 | "#f4cb29", 66 | "#f4cb29" 67 | ], 68 | "text_color_disabled": [ 69 | "#f4cb29", 70 | "#f4cb29" 71 | ] 72 | }, 73 | "CTkEntry": { 74 | "corner_radius": 6, 75 | "border_width": 2, 76 | "fg_color": [ 77 | "#38261b", 78 | "#38261b" 79 | ], 80 | "border_color": [ 81 | "#f4cb29", 82 | "#f4cb29" 83 | ], 84 | "text_color": [ 85 | "#fe9433", 86 | "#fe9433" 87 | ], 88 | "placeholder_text_color": [ 89 | "#98591f", 90 | "#98591f" 91 | ] 92 | }, 93 | "CTkSwitch": { 94 | "corner_radius": 1000, 95 | "border_width": 3, 96 | "button_length": 0, 97 | "progress_color": [ 98 | "#6d4934", 99 | "#6d4934" 100 | ], 101 | "button_color": [ 102 | "#fe9433", 103 | "#fe9433" 104 | ], 105 | "fg_color": [ 106 | "#663b14", 107 | "#663b14" 108 | ], 109 | "button_hover_color": [ 110 | "#fea95c", 111 | "#fea95c" 112 | ], 113 | "text_color": [ 114 | "#f4cb29", 115 | "#f4cb29" 116 | ], 117 | "text_color_disabled": [ 118 | "#98591f", 119 | "#98591f" 120 | ] 121 | }, 122 | "CTkProgressBar": { 123 | "corner_radius": 1000, 124 | "border_width": 0, 125 | "fg_color": [ 126 | "#532000", 127 | "#532000" 128 | ], 129 | "progress_color": [ 130 | "#e5852e", 131 | "#e5852e" 132 | ], 133 | "border_color": [ 134 | "#0b0000", 135 | "#0b0000" 136 | ] 137 | }, 138 | "CTkSlider": { 139 | "corner_radius": 1000, 140 | "button_corner_radius": 1000, 141 | "border_width": 6, 142 | "button_length": 0, 143 | "fg_color": [ 144 | "#532000", 145 | "#532000" 146 | ], 147 | "progress_color": [ 148 | "#6d4934", 149 | "#6d4934" 150 | ], 151 | "button_color": [ 152 | "#fe9433", 153 | "#fe9433" 154 | ], 155 | "button_hover_color": [ 156 | "#fea95c", 157 | "#fea95c" 158 | ] 159 | }, 160 | "CTkOptionMenu": { 161 | "corner_radius": 6, 162 | "fg_color": [ 163 | "#532000", 164 | "#532000" 165 | ], 166 | "button_color": [ 167 | "#3a1600", 168 | "#3a1600" 169 | ], 170 | "button_hover_color": [ 171 | "#754d33", 172 | "#754d33" 173 | ], 174 | "text_color": [ 175 | "#f4cb29", 176 | "#f4cb29" 177 | ], 178 | "text_color_disabled": [ 179 | "#cb7629", 180 | "#cb7629" 181 | ] 182 | }, 183 | "CTkComboBox": { 184 | "corner_radius": 6, 185 | "border_width": 2, 186 | "fg_color": [ 187 | "#38261b", 188 | "#38261b" 189 | ], 190 | "border_color": [ 191 | "#f4cb29", 192 | "#f4cb29" 193 | ], 194 | "button_color": [ 195 | "#b88f06", 196 | "#b88f06" 197 | ], 198 | "button_hover_color": [ 199 | "#754d33", 200 | "#754d33" 201 | ], 202 | "text_color": [ 203 | "#f4cb29", 204 | "#f4cb29" 205 | ], 206 | "text_color_disabled": [ 207 | "#cb7629", 208 | "#cb7629" 209 | ] 210 | }, 211 | "CTkScrollbar": { 212 | "corner_radius": 1000, 213 | "border_spacing": 4, 214 | "fg_color": "transparent", 215 | "button_color": [ 216 | "#Fe9433", 217 | "#Fe9433" 218 | ], 219 | "button_hover_color": [ 220 | "#f4cb29", 221 | "#f4cb29" 222 | ] 223 | }, 224 | "CTkSegmentedButton": { 225 | "corner_radius": 6, 226 | "border_width": 2, 227 | "fg_color": [ 228 | "#8b5626", 229 | "#8b5626" 230 | ], 231 | "selected_color": [ 232 | "#814007", 233 | "#814007" 234 | ], 235 | "selected_hover_color": [ 236 | "#8b5626", 237 | "#8b5626" 238 | ], 239 | "unselected_color": [ 240 | "#2a1000", 241 | "#2a1000" 242 | ], 243 | "unselected_hover_color": [ 244 | "#46220c", 245 | "#46220c" 246 | ], 247 | "text_color": [ 248 | "#f4cb29", 249 | "#f4cb29" 250 | ], 251 | "text_color_disabled": [ 252 | "#cb7629", 253 | "#cb7629" 254 | ] 255 | }, 256 | "CTkTextbox": { 257 | "corner_radius": 6, 258 | "border_width": 2, 259 | "fg_color": [ 260 | "#38261b", 261 | "#38261b" 262 | ], 263 | "border_color": [ 264 | "#f4cb29", 265 | "#f4cb29" 266 | ], 267 | "text_color": [ 268 | "#Fe9433", 269 | "#Fe9433" 270 | ], 271 | "scrollbar_button_color": [ 272 | "#Fe9433", 273 | "#Fe9433" 274 | ], 275 | "scrollbar_button_hover_color": [ 276 | "#f4cb29", 277 | "#f4cb29" 278 | ] 279 | }, 280 | "CTkScrollableFrame": { 281 | "label_fg_color": [ 282 | "#532000", 283 | "#532000" 284 | ] 285 | }, 286 | "DropdownMenu": { 287 | "fg_color": [ 288 | "#d8b35e", 289 | "#d8b35e" 290 | ], 291 | "hover_color": [ 292 | "#b88f06", 293 | "#b88f06" 294 | ], 295 | "text_color": [ 296 | "gray10", 297 | "gray10" 298 | ] 299 | }, 300 | "CTkFont": { 301 | "macOS": { 302 | "family": "SF Display", 303 | "size": 13, 304 | "weight": "normal" 305 | }, 306 | "Windows": { 307 | "family": "Roboto", 308 | "size": 13, 309 | "weight": "normal" 310 | }, 311 | "Linux": { 312 | "family": "Roboto", 313 | "size": 13, 314 | "weight": "normal" 315 | } 316 | }, 317 | "provenance": { 318 | "theme name": "NeonBanana", 319 | "theme author": "clive", 320 | "date created": "Jun 14 2023 20:24:54", 321 | "last modified by": "Clive Bostock", 322 | "created with": "CTk Theme Builder v2.3.0", 323 | "last modified": "Aug 13 2023 13:53:38", 324 | "keystone colour": "#3c5064", 325 | "harmony method": "Split-complementary", 326 | "harmony differential": null 327 | }, 328 | "CTkCheckBox": { 329 | "corner_radius": 6, 330 | "border_width": 3, 331 | "fg_color": [ 332 | "#e5852e", 333 | "#e5852e" 334 | ], 335 | "border_color": [ 336 | "#532000", 337 | "#532000" 338 | ], 339 | "hover_color": [ 340 | "#e5852e", 341 | "#e5852e" 342 | ], 343 | "checkmark_color": [ 344 | "#0a0a0a", 345 | "#0a0a0a" 346 | ], 347 | "text_color": [ 348 | "#f4cb29", 349 | "#f4cb29" 350 | ], 351 | "text_color_disabled": [ 352 | "#98591f", 353 | "#98591f" 354 | ] 355 | }, 356 | "CTkRadioButton": { 357 | "corner_radius": 1000, 358 | "border_width_checked": 6, 359 | "border_width_unchecked": 3, 360 | "fg_color": [ 361 | "#e5852e", 362 | "#e5852e" 363 | ], 364 | "border_color": [ 365 | "#c0ad10", 366 | "#c0ad10" 367 | ], 368 | "hover_color": [ 369 | "#e5852e", 370 | "#e5852e" 371 | ], 372 | "text_color": [ 373 | "#f4cb29", 374 | "#f4cb29" 375 | ], 376 | "text_color_disabled": [ 377 | "#98591f", 378 | "#98591f" 379 | ] 380 | } 381 | } -------------------------------------------------------------------------------- /boiiiwd_package/resources/boiiiwd_obsidian.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOIIIWD_Globals": { 3 | "button_active_state_color": "#070d13", 4 | "button_normal_state_color": "#11202b", 5 | "progress_bar_fill_color": "#11202b", 6 | "this_is_a_comment": "For button hover color check CTkButton bellow + other stuff", 7 | "credits": "originally by avalon60" 8 | }, 9 | "CTk": { 10 | "fg_color": [ 11 | "#030303", 12 | "#030303" 13 | ] 14 | }, 15 | "CTkToplevel": { 16 | "fg_color": [ 17 | "#030303", 18 | "#030303" 19 | ] 20 | }, 21 | "CTkFrame": { 22 | "corner_radius": 6, 23 | "border_width": 1, 24 | "fg_color": [ 25 | "#111111", 26 | "#030303" 27 | ], 28 | "top_fg_color": [ 29 | "#131313", 30 | "#010101" 31 | ], 32 | "border_color": [ 33 | "#586b78", 34 | "#586b78" 35 | ] 36 | }, 37 | "CTkButton": { 38 | "corner_radius": 6, 39 | "border_width": 2, 40 | "fg_color": [ 41 | "#11202b", 42 | "#11202b" 43 | ], 44 | "hover_color": [ 45 | "#404c59", 46 | "#404c59" 47 | ], 48 | "border_color": [ 49 | "#405366", 50 | "#405366" 51 | ], 52 | "text_color": [ 53 | "#ffffff", 54 | "#ffffff" 55 | ], 56 | "text_color_disabled": [ 57 | "#b4bcc6", 58 | "#b4bcc6" 59 | ] 60 | }, 61 | "CTkLabel": { 62 | "corner_radius": 0, 63 | "fg_color": "transparent", 64 | "text_color": [ 65 | "#c3cbd5", 66 | "#c3cbd5" 67 | ], 68 | "text_color_disabled": [ 69 | "#c3cbd5", 70 | "#c3cbd5" 71 | ] 72 | }, 73 | "CTkEntry": { 74 | "corner_radius": 6, 75 | "border_width": 2, 76 | "fg_color": [ 77 | "#11202b", 78 | "#11202b" 79 | ], 80 | "border_color": [ 81 | "#000000", 82 | "#000000" 83 | ], 84 | "text_color": [ 85 | "white", 86 | "white" 87 | ], 88 | "placeholder_text_color": [ 89 | "#464e56", 90 | "#464e56" 91 | ] 92 | }, 93 | "CTkSwitch": { 94 | "corner_radius": 1000, 95 | "border_width": 3, 96 | "button_length": 0, 97 | "progress_color": [ 98 | "#748498", 99 | "#748498" 100 | ], 101 | "button_color": [ 102 | "#172631", 103 | "#11202b" 104 | ], 105 | "fg_color": [ 106 | "#383e3e", 107 | "#383e3e" 108 | ], 109 | "button_hover_color": [ 110 | "#5c6b78", 111 | "#1b2631" 112 | ], 113 | "text_color": [ 114 | "#c3cbd5", 115 | "#c3cbd5" 116 | ], 117 | "text_color_disabled": [ 118 | "#5e6b7c", 119 | "#b4bcc6" 120 | ] 121 | }, 122 | "CTkProgressBar": { 123 | "corner_radius": 1000, 124 | "border_width": 0, 125 | "fg_color": [ 126 | "#383e3e", 127 | "#383e3e" 128 | ], 129 | "progress_color": [ 130 | "#748498", 131 | "#748498" 132 | ], 133 | "border_color": [ 134 | "#000000", 135 | "#000000" 136 | ] 137 | }, 138 | "CTkSlider": { 139 | "corner_radius": 1000, 140 | "button_corner_radius": 1000, 141 | "border_width": 6, 142 | "button_length": 0, 143 | "fg_color": [ 144 | "#383e3e", 145 | "#383e3e" 146 | ], 147 | "progress_color": [ 148 | "#748498", 149 | "#748498" 150 | ], 151 | "button_color": [ 152 | "#172631", 153 | "#11202b" 154 | ], 155 | "button_hover_color": [ 156 | "#5c6b78", 157 | "#5c6b78" 158 | ] 159 | }, 160 | "CTkOptionMenu": { 161 | "corner_radius": 6, 162 | "fg_color": [ 163 | "#748498", 164 | "#748498" 165 | ], 166 | "button_color": [ 167 | "#2b3948", 168 | "#2b3948" 169 | ], 170 | "button_hover_color": [ 171 | "#586b78", 172 | "#586b78" 173 | ], 174 | "text_color": [ 175 | "#ffffff", 176 | "#ffffff" 177 | ], 178 | "text_color_disabled": [ 179 | "#5e6b7c", 180 | "#b4bcc6" 181 | ] 182 | }, 183 | "CTkComboBox": { 184 | "corner_radius": 6, 185 | "border_width": 2, 186 | "fg_color": [ 187 | "#748498", 188 | "#748498" 189 | ], 190 | "border_color": [ 191 | "#000000", 192 | "#000000" 193 | ], 194 | "button_color": [ 195 | "#000000", 196 | "#000000" 197 | ], 198 | "button_hover_color": [ 199 | "#5c636c", 200 | "#5c636c" 201 | ], 202 | "text_color": [ 203 | "#ffffff", 204 | "#ffffff" 205 | ], 206 | "text_color_disabled": [ 207 | "#5e6b7c", 208 | "#b4bcc6" 209 | ] 210 | }, 211 | "CTkScrollbar": { 212 | "corner_radius": 1000, 213 | "border_spacing": 4, 214 | "fg_color": "transparent", 215 | "button_color": [ 216 | "#35444f", 217 | "#4e5c6e" 218 | ], 219 | "button_hover_color": [ 220 | "#404c59", 221 | "#1c1e18" 222 | ] 223 | }, 224 | "CTkSegmentedButton": { 225 | "corner_radius": 6, 226 | "border_width": 2, 227 | "fg_color": [ 228 | "#748498", 229 | "#748498" 230 | ], 231 | "selected_color": [ 232 | "#c6ced8", 233 | "#c6ced8" 234 | ], 235 | "selected_hover_color": [ 236 | "#d3dbe1", 237 | "#d3dbe1" 238 | ], 239 | "unselected_color": [ 240 | "#697b88", 241 | "#697b88" 242 | ], 243 | "unselected_hover_color": [ 244 | "#586b78", 245 | "#586b78" 246 | ], 247 | "text_color": [ 248 | "#2b3449", 249 | "#2b3449" 250 | ], 251 | "text_color_disabled": [ 252 | "#464e58", 253 | "#3c444e" 254 | ] 255 | }, 256 | "CTkTextbox": { 257 | "corner_radius": 6, 258 | "border_width": 0, 259 | "fg_color": [ 260 | "#11202b", 261 | "#11202b" 262 | ], 263 | "border_color": [ 264 | "#000000", 265 | "#000000" 266 | ], 267 | "text_color": [ 268 | "white", 269 | "white" 270 | ], 271 | "scrollbar_button_color": [ 272 | "#1a2934", 273 | "#11202b" 274 | ], 275 | "scrollbar_button_hover_color": [ 276 | "#404c59", 277 | "#404c59" 278 | ] 279 | }, 280 | "CTkScrollableFrame": { 281 | "label_fg_color": [ 282 | "#0e1d28", 283 | "#0e1d28" 284 | ] 285 | }, 286 | "DropdownMenu": { 287 | "fg_color": [ 288 | "#748498", 289 | "#748498" 290 | ], 291 | "hover_color": [ 292 | "#d3dbe1", 293 | "#d3dbe1" 294 | ], 295 | "text_color": [ 296 | "#000000", 297 | "#000000" 298 | ] 299 | }, 300 | "CTkFont": { 301 | "macOS": { 302 | "family": "SF Display", 303 | "size": 13, 304 | "weight": "normal" 305 | }, 306 | "Windows": { 307 | "family": "Roboto", 308 | "size": 13, 309 | "weight": "normal" 310 | }, 311 | "Linux": { 312 | "family": "Roboto", 313 | "size": 13, 314 | "weight": "normal" 315 | } 316 | }, 317 | "provenance": { 318 | "theme name": "Anthracite", 319 | "theme author": "clive", 320 | "date created": "Jun 10 2023 12:22:59", 321 | "last modified by": "clive", 322 | "created with": "CTk Theme Builder v2.2.0", 323 | "last modified": "Aug 13 2023 12:50:49", 324 | "keystone colour": "#3c5064", 325 | "harmony method": "Split-complementary", 326 | "harmony differential": null 327 | }, 328 | "CTkCheckBox": { 329 | "corner_radius": 6, 330 | "border_width": 3, 331 | "fg_color": [ 332 | "#748498", 333 | "#748498" 334 | ], 335 | "border_color": [ 336 | "#3e4654", 337 | "#3e4654" 338 | ], 339 | "hover_color": [ 340 | "#697b88", 341 | "#697b88" 342 | ], 343 | "checkmark_color": [ 344 | "#ffffff", 345 | "#ffffff" 346 | ], 347 | "text_color": [ 348 | "#c3cbd5", 349 | "#c3cbd5" 350 | ], 351 | "text_color_disabled": [ 352 | "#5e6b7c", 353 | "#b4bcc6" 354 | ] 355 | }, 356 | "CTkRadioButton": { 357 | "corner_radius": 1000, 358 | "border_width_checked": 6, 359 | "border_width_unchecked": 3, 360 | "fg_color": [ 361 | "#172631", 362 | "#11202b" 363 | ], 364 | "border_color": [ 365 | "#303940", 366 | "#303940" 367 | ], 368 | "hover_color": [ 369 | "#5c6b78", 370 | "#5c6b78" 371 | ], 372 | "text_color": [ 373 | "#c3cbd5", 374 | "#c3cbd5" 375 | ], 376 | "text_color_disabled": [ 377 | "#c8d0de", 378 | "#b4bcc6" 379 | ] 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /boiiiwd_package/resources/boiiiwd_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOIIIWD_Globals": { 3 | "button_active_state_color": "#070d13", 4 | "button_normal_state_color": "#11202b", 5 | "progress_bar_fill_color": "#11202b", 6 | "this_is_a_comment": "For button hover color check CTkButton bellow + other stuff", 7 | "credits": "originally by avalon60" 8 | }, 9 | "CTk": { 10 | "fg_color": [ 11 | "#2d4380", 12 | "#233976" 13 | ] 14 | }, 15 | "CTkToplevel": { 16 | "fg_color": [ 17 | "#324885", 18 | "#233976" 19 | ] 20 | }, 21 | "CTkFrame": { 22 | "corner_radius": 6, 23 | "border_width": 3, 24 | "fg_color": [ 25 | "#3c528f", 26 | "#233976" 27 | ], 28 | "top_fg_color": [ 29 | "#324885", 30 | "#192f6c" 31 | ], 32 | "border_color": [ 33 | "#192d69", 34 | "#3F5EA8" 35 | ] 36 | }, 37 | "CTkButton": { 38 | "corner_radius": 6, 39 | "border_width": 0, 40 | "fg_color": [ 41 | "#5170ba", 42 | "#142864" 43 | ], 44 | "hover_color": [ 45 | "#8c6b3f", 46 | "#8c6b3f" 47 | ], 48 | "border_color": [ 49 | "#192d69", 50 | "#3F5EA8" 51 | ], 52 | "text_color": [ 53 | "#ffffff", 54 | "#ffffff" 55 | ], 56 | "text_color_disabled": [ 57 | "#b1d9ff", 58 | "#b1d9ff" 59 | ] 60 | }, 61 | "CTkLabel": { 62 | "corner_radius": 0, 63 | "fg_color": "transparent", 64 | "text_color": [ 65 | "#ffffff", 66 | "#ffffff" 67 | ], 68 | "text_color_disabled": [ 69 | "#ffffff", 70 | "#ffffff" 71 | ] 72 | }, 73 | "CTkEntry": { 74 | "corner_radius": 6, 75 | "border_width": 2, 76 | "fg_color": [ 77 | "#283e7b", 78 | "#1e326e" 79 | ], 80 | "border_color": [ 81 | "#192d69", 82 | "#3F5EA8" 83 | ], 84 | "text_color": [ 85 | "#ffffff", 86 | "#ffffff" 87 | ], 88 | "placeholder_text_color": [ 89 | "#a3c8ff", 90 | "#a3c8ff" 91 | ] 92 | }, 93 | "CTkSwitch": { 94 | "corner_radius": 1000, 95 | "border_width": 3, 96 | "button_length": 0, 97 | "fg_color": [ 98 | "#8c6b3f", 99 | "#8c6b3f" 100 | ], 101 | "progress_color": [ 102 | "#3FA854", 103 | "#3FA854" 104 | ], 105 | "button_color": [ 106 | "#283e7b", 107 | "#142864" 108 | ], 109 | "button_hover_color": [ 110 | "#8c6b3f", 111 | "#8c6b3f" 112 | ], 113 | "text_color": [ 114 | "#ffffff", 115 | "#ffffff" 116 | ], 117 | "text_color_disabled": [ 118 | "#b1d9ff", 119 | "#b1d9ff" 120 | ] 121 | }, 122 | "CTkProgressBar": { 123 | "corner_radius": 1000, 124 | "border_width": 0, 125 | "fg_color": [ 126 | "#8c6b3f", 127 | "#8c6b3f" 128 | ], 129 | "progress_color": [ 130 | "#3FA854", 131 | "#3FA854" 132 | ], 133 | "border_color": [ 134 | "#192d69", 135 | "#3F5EA8" 136 | ] 137 | }, 138 | "CTkSlider": { 139 | "corner_radius": 1000, 140 | "button_corner_radius": 1000, 141 | "border_width": 6, 142 | "button_length": 0, 143 | "fg_color": [ 144 | "#8c6b3f", 145 | "#8c6b3f" 146 | ], 147 | "progress_color": [ 148 | "#3FA854", 149 | "#3FA854" 150 | ], 151 | "button_color": [ 152 | "#3F5EA8", 153 | "#3F5EA8" 154 | ], 155 | "button_hover_color": [ 156 | "#8c6b3f", 157 | "#8c6b3f" 158 | ] 159 | }, 160 | "CTkOptionMenu": { 161 | "corner_radius": 6, 162 | "fg_color": [ 163 | "#233976", 164 | "#192f6c" 165 | ], 166 | "button_color": [ 167 | "#36a450", 168 | "#36a450" 169 | ], 170 | "button_hover_color": [ 171 | "#8c6b3f", 172 | "#8c6b3f" 173 | ], 174 | "text_color": [ 175 | "#ffffff", 176 | "#ffffff" 177 | ], 178 | "text_color_disabled": [ 179 | "#b1d9ff", 180 | "#b1d9ff" 181 | ] 182 | }, 183 | "CTkComboBox": { 184 | "corner_radius": 6, 185 | "border_width": 2, 186 | "fg_color": [ 187 | "#283e7b", 188 | "#233773" 189 | ], 190 | "border_color": [ 191 | "#192d69", 192 | "#3F5EA8" 193 | ], 194 | "button_color": [ 195 | "#5170ba", 196 | "#142864" 197 | ], 198 | "button_hover_color": [ 199 | "#8c6b3f", 200 | "#8c6b3f" 201 | ], 202 | "text_color": [ 203 | "#ffffff", 204 | "#ffffff" 205 | ], 206 | "text_color_disabled": [ 207 | "#b1d9ff", 208 | "#b1d9ff" 209 | ] 210 | }, 211 | "CTkScrollbar": { 212 | "corner_radius": 1000, 213 | "border_spacing": 4, 214 | "fg_color": "transparent", 215 | "button_color": [ 216 | "#6c8bd5", 217 | "#6c8bd5" 218 | ], 219 | "button_hover_color": [ 220 | "#8c6b3f", 221 | "#8c6b3f" 222 | ] 223 | }, 224 | "CTkSegmentedButton": { 225 | "corner_radius": 6, 226 | "border_width": 2, 227 | "fg_color": [ 228 | "#283e7b", 229 | "#142864" 230 | ], 231 | "selected_color": [ 232 | "#3F5EA8", 233 | "#3F5EA8" 234 | ], 235 | "selected_hover_color": [ 236 | "#8c6b3f", 237 | "#8c6b3f" 238 | ], 239 | "unselected_color": [ 240 | "#112c7c", 241 | "#112c7c" 242 | ], 243 | "unselected_hover_color": [ 244 | "#8c6b3f", 245 | "#8c6b3f" 246 | ], 247 | "text_color": [ 248 | "#ffffff", 249 | "#ffffff" 250 | ], 251 | "text_color_disabled": [ 252 | "#b1d9ff", 253 | "#b1d9ff" 254 | ] 255 | }, 256 | "CTkTextbox": { 257 | "corner_radius": 6, 258 | "border_width": 2, 259 | "fg_color": [ 260 | "#283e7b", 261 | "#1e326e" 262 | ], 263 | "border_color": [ 264 | "#192d69", 265 | "#3F5EA8" 266 | ], 267 | "text_color": [ 268 | "#ffffff", 269 | "#ffffff" 270 | ], 271 | "scrollbar_button_color": [ 272 | "#6c8bd5", 273 | "#6c8bd5" 274 | ], 275 | "scrollbar_button_hover_color": [ 276 | "#8c6b3f", 277 | "#8c6b3f" 278 | ] 279 | }, 280 | "CTkScrollableFrame": { 281 | "label_fg_color": [ 282 | "#324885", 283 | "#192f6c" 284 | ] 285 | }, 286 | "DropdownMenu": { 287 | "fg_color": [ 288 | "#283e7b", 289 | "#283e7b" 290 | ], 291 | "hover_color": [ 292 | "#112c7c", 293 | "#112c7c" 294 | ], 295 | "text_color": [ 296 | "#a3c8ff", 297 | "#a3c8ff" 298 | ] 299 | }, 300 | "CTkFont": { 301 | "macOS": { 302 | "family": "SF Display", 303 | "size": 13, 304 | "weight": "normal" 305 | }, 306 | "Windows": { 307 | "family": "Roboto", 308 | "size": 13, 309 | "weight": "normal" 310 | }, 311 | "Linux": { 312 | "family": "Roboto", 313 | "size": 13, 314 | "weight": "normal" 315 | } 316 | }, 317 | "provenance": { 318 | "theme author": "Clive Bostock", 319 | "date created": "Mar 27 2023 18:16:02", 320 | "last modified by": "Clive Bostock", 321 | "created with": "CTk Theme Builder", 322 | "last modified": "Aug 13 2023 14:03:54", 323 | "keystone colour": "#1f3572", 324 | "harmony method": "Complementary", 325 | "harmony differential": 5, 326 | "theme name": "TrojanBlue" 327 | }, 328 | "CTkCheckBox": { 329 | "corner_radius": 6, 330 | "border_width": 3, 331 | "fg_color": [ 332 | "#283e7b", 333 | "#142864" 334 | ], 335 | "border_color": [ 336 | "#192d69", 337 | "#3F5EA8" 338 | ], 339 | "hover_color": [ 340 | "#112c7c", 341 | "#112c7c" 342 | ], 343 | "checkmark_color": [ 344 | "#ffffff", 345 | "#ffffff" 346 | ], 347 | "text_color": [ 348 | "#ffffff", 349 | "#ffffff" 350 | ], 351 | "text_color_disabled": [ 352 | "#b1d9ff", 353 | "#b1d9ff" 354 | ] 355 | }, 356 | "CTkRadioButton": { 357 | "corner_radius": 1000, 358 | "border_width_checked": 6, 359 | "border_width_unchecked": 3, 360 | "fg_color": [ 361 | "#3FA854", 362 | "#3FA854" 363 | ], 364 | "border_color": [ 365 | "#192d69", 366 | "#3F5EA8" 367 | ], 368 | "hover_color": [ 369 | "#8c6b3f", 370 | "#8c6b3f" 371 | ], 372 | "text_color": [ 373 | "#ffffff", 374 | "#d9d9d9" 375 | ], 376 | "text_color_disabled": [ 377 | "#b1d9ff", 378 | "#b1d9ff" 379 | ] 380 | } 381 | } -------------------------------------------------------------------------------- /boiiiwd_package/resources/boiiiwd_theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOIIIWD_Globals": { 3 | "button_active_state_color": "#351400", 4 | "button_normal_state_color": "#d35600", 5 | "progress_bar_fill_color": "#d35600", 6 | "this_is_a_comment": "For button hover color check CTkButton bellow + other stuff" 7 | }, 8 | "CTk": { 9 | "fg_color": ["gray95", "gray10"] 10 | }, 11 | "CTkToplevel": { 12 | "fg_color": ["gray95", "gray10"] 13 | }, 14 | "CTkFrame": { 15 | "corner_radius": 6, 16 | "border_width": 0, 17 | "fg_color": ["gray90", "gray13"], 18 | "top_fg_color": ["gray85", "gray16"], 19 | "border_color": ["gray65", "gray28"] 20 | }, 21 | "CTkButton": { 22 | "corner_radius": 6, 23 | "border_width": 0, 24 | "fg_color": ["#d35600", "#d35600"], 25 | "hover_color": ["#863600", "#863600"], 26 | "border_color": ["#3E454A", "#949A9F"], 27 | "text_color": ["#fcfcfc", "#fcfcfc"], 28 | "text_color_disabled": ["gray74", "gray60"] 29 | }, 30 | "CTkLabel": { 31 | "corner_radius": 0, 32 | "fg_color": "transparent", 33 | "text_color": ["gray14", "gray84"] 34 | }, 35 | "CTkEntry": { 36 | "corner_radius": 6, 37 | "border_width": 2, 38 | "fg_color": ["#F9F9FA", "#343638"], 39 | "border_color": ["#979DA2", "#565B5E"], 40 | "text_color": ["gray14", "gray84"], 41 | "placeholder_text_color": ["gray52", "gray62"] 42 | }, 43 | "CTkCheckBox": { 44 | "corner_radius": 6, 45 | "border_width": 3, 46 | "fg_color": ["#3a3a3a", "#3a3a3a"], 47 | "hover_color": ["#737373", "#737373"], 48 | "border_color": ["#3E454A", "#949A9F"], 49 | "checkmark_color": ["#DCE4EE", "gray90"], 50 | "text_color": ["gray14", "gray84"], 51 | "text_color_disabled": ["gray60", "gray45"] 52 | }, 53 | "CTkSwitch": { 54 | "corner_radius": 1000, 55 | "border_width": 3, 56 | "button_length": 0, 57 | "fg_color": ["#3a3a3a", "#3a3a3a"], 58 | "progress_color": ["#d35600", "#d35600"], 59 | "button_color": ["gray36", "#D5D9DE"], 60 | "button_hover_color": ["gray20", "gray100"], 61 | "text_color": ["gray14", "gray84"], 62 | "text_color_disabled": ["gray60", "gray45"] 63 | }, 64 | "CTkRadioButton": { 65 | "corner_radius": 1000, 66 | "border_width_checked": 6, 67 | "border_width_unchecked": 3, 68 | "fg_color": ["#3a3a3a", "#3a3a3a"], 69 | "border_color": ["#3E454A", "#949A9F"], 70 | "hover_color": ["#325882", "#14375e"], 71 | "text_color": ["gray14", "gray84"], 72 | "text_color_disabled": ["gray60", "gray45"] 73 | }, 74 | "CTkProgressBar": { 75 | "corner_radius": 1000, 76 | "border_width": 0, 77 | "fg_color": ["#3a3a3a", "#3a3a3a"], 78 | "progress_color": ["#3a7ebf", "#1f538d"], 79 | "border_color": ["gray", "gray"] 80 | }, 81 | "CTkSlider": { 82 | "corner_radius": 1000, 83 | "button_corner_radius": 1000, 84 | "border_width": 6, 85 | "button_length": 0, 86 | "fg_color": ["#939BA2", "#4A4D50"], 87 | "progress_color": ["gray40", "#AAB0B5"], 88 | "button_color": ["#3a7ebf", "#1f538d"], 89 | "button_hover_color": ["#325882", "#14375e"] 90 | }, 91 | "CTkOptionMenu": { 92 | "corner_radius": 6, 93 | "fg_color": ["#3a3a3a", "#3a3a3a"], 94 | "button_color": ["#d35600", "#d35600"], 95 | "button_hover_color": ["#343638", "#343638"], 96 | "text_color": ["#DCE4EE", "#DCE4EE"], 97 | "text_color_disabled": ["gray74", "gray60"] 98 | }, 99 | "CTkComboBox": { 100 | "corner_radius": 6, 101 | "border_width": 2, 102 | "fg_color": ["#F9F9FA", "#343638"], 103 | "border_color": ["#979DA2", "#565B5E"], 104 | "button_color": ["#979DA2", "#565B5E"], 105 | "button_hover_color": ["#6E7174", "#7A848D"], 106 | "text_color": ["gray14", "gray84"], 107 | "text_color_disabled": ["gray50", "gray45"] 108 | }, 109 | "CTkScrollbar": { 110 | "corner_radius": 1000, 111 | "border_spacing": 4, 112 | "fg_color": "transparent", 113 | "button_color": ["gray55", "gray41"], 114 | "button_hover_color": ["gray40", "gray53"] 115 | }, 116 | "CTkSegmentedButton": { 117 | "corner_radius": 6, 118 | "border_width": 2, 119 | "fg_color": ["#979DA2", "gray29"], 120 | "selected_color": ["#3a7ebf", "#1f538d"], 121 | "selected_hover_color": ["#325882", "#14375e"], 122 | "unselected_color": ["#979DA2", "gray29"], 123 | "unselected_hover_color": ["gray70", "gray41"], 124 | "text_color": ["#DCE4EE", "#DCE4EE"], 125 | "text_color_disabled": ["gray74", "gray60"] 126 | }, 127 | "CTkTextbox": { 128 | "corner_radius": 6, 129 | "border_width": 0, 130 | "fg_color": ["gray100", "gray20"], 131 | "border_color": ["#979DA2", "#565B5E"], 132 | "text_color": ["gray14", "gray84"], 133 | "scrollbar_button_color": ["gray55", "gray41"], 134 | "scrollbar_button_hover_color": ["gray40", "gray53"] 135 | }, 136 | "CTkScrollableFrame": { 137 | "label_fg_color": ["gray80", "gray21"] 138 | }, 139 | "DropdownMenu": { 140 | "fg_color": ["gray90", "gray20"], 141 | "hover_color": ["gray75", "gray28"], 142 | "text_color": ["gray14", "gray84"] 143 | }, 144 | "CTkFont": { 145 | "macOS": { 146 | "family": "SF Display", 147 | "size": 13, 148 | "weight": "normal" 149 | }, 150 | "Windows": { 151 | "family": "Roboto", 152 | "size": 13, 153 | "weight": "normal" 154 | }, 155 | "Linux": { 156 | "family": "Roboto", 157 | "size": 13, 158 | "weight": "normal" 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /boiiiwd_package/resources/default_library_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/default_library_img.png -------------------------------------------------------------------------------- /boiiiwd_package/resources/map_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/map_image.png -------------------------------------------------------------------------------- /boiiiwd_package/resources/mod_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/mod_image.png -------------------------------------------------------------------------------- /boiiiwd_package/resources/ryuk.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/ryuk.ico -------------------------------------------------------------------------------- /boiiiwd_package/resources/ryuk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/ryuk.png -------------------------------------------------------------------------------- /boiiiwd_package/resources/sett10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/sett10.png -------------------------------------------------------------------------------- /boiiiwd_package/resources/update_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/boiiiwd_package/resources/update_icon.png -------------------------------------------------------------------------------- /boiiiwd_package/src/CTkListbox/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CustomTkinter Listbox 3 | Author: Akash Bora (Akascape) 4 | License: MIT 5 | Homepage: https://github.com/Akascape/CTkListbox 6 | """ 7 | 8 | __version__ = '0.9' 9 | 10 | from .ctk_listbox import CTkListbox 11 | -------------------------------------------------------------------------------- /boiiiwd_package/src/CTkListbox/ctk_listbox.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom ListBox for customtkinter 3 | Author: Akash Bora 4 | """ 5 | 6 | import customtkinter 7 | 8 | class CTkListbox(customtkinter.CTkScrollableFrame): 9 | 10 | def __init__(self, 11 | master: any, 12 | height: int = 100, 13 | width: int = 200, 14 | hightlight_color: str = "default", 15 | fg_color: str = "transparent", 16 | bg_color: str = None, 17 | text_color: str = "default", 18 | select_color: str = "default", 19 | hover_color: str = "default", 20 | border_width: int = 3, 21 | font: tuple = "default", 22 | multiple_selection: bool = False, 23 | listvariable = None, 24 | hover: bool = True, 25 | command = None, 26 | justify = "left", 27 | **kwargs): 28 | 29 | super().__init__(master, width=width, height=height, fg_color=fg_color, border_width=border_width, **kwargs) 30 | self._scrollbar.grid_configure(padx=(0,border_width)) 31 | self._scrollbar.configure(width=12) 32 | 33 | if bg_color: 34 | super().configure(bg_color=bg_color) 35 | 36 | self.select_color = customtkinter.ThemeManager.theme["CTkButton"]["fg_color"] if select_color=="default" else select_color 37 | self.text_color = customtkinter.ThemeManager.theme["CTkButton"]["text_color"] if text_color=="default" else text_color 38 | self.hover_color = customtkinter.ThemeManager.theme["CTkButton"]["hover_color"] if hover_color=="default" else hover_color 39 | self.font = (customtkinter.ThemeManager.theme["CTkFont"]["family"],13) if font=="default" else font 40 | if justify=="left": 41 | self.justify = "w" 42 | elif justify=="right": 43 | self.justify = "e" 44 | else: 45 | self.justify = "c" 46 | self.buttons = {} 47 | self.command = command 48 | self.multiple = multiple_selection 49 | self.selected = None 50 | self.hover = hover 51 | self.end_num = 0 52 | self.selections = [] 53 | 54 | if listvariable: 55 | self.listvariable = listvariable 56 | self.listvariable.trace_add('write', lambda a,b,c: self.update_listvar()) 57 | self.update_listvar() 58 | 59 | super().bind("", lambda e: self.unbind_all("")) 60 | 61 | def update_listvar(self): 62 | values = list(eval(self.listvariable.get())) 63 | self.delete("all") 64 | for i in values: 65 | self.insert("END", option=i) 66 | 67 | def select(self, index): 68 | """ select the option """ 69 | for options in self.buttons.values(): 70 | options.configure(fg_color="transparent") 71 | 72 | if self.multiple: 73 | if self.buttons[index] in self.selections: 74 | self.selections.remove(self.buttons[index]) 75 | self.buttons[index].configure(fg_color="transparent", hover=False) 76 | self.after(100, lambda: self.buttons[index].configure(hover=self.hover)) 77 | else: 78 | self.selections.append(self.buttons[index]) 79 | for i in self.selections: 80 | i.configure(fg_color=self.select_color, hover=False) 81 | self.after(100, lambda button=i: button.configure(hover=self.hover)) 82 | else: 83 | self.selected = self.buttons[index] 84 | self.buttons[index].configure(fg_color=self.select_color, hover=False) 85 | self.after(100, lambda: self.buttons[index].configure(hover=self.hover)) 86 | 87 | if self.command: 88 | self.command(self.get()) 89 | 90 | def activate(self, index): 91 | if str(index).lower()=="all": 92 | if self.multiple: 93 | for i in self.buttons: 94 | self.select(i) 95 | return 96 | selected = list(self.buttons.keys())[index] 97 | self.select(selected) 98 | 99 | def curselection(self): 100 | index = 0 101 | if self.multiple: 102 | indexes = [] 103 | for i in self.buttons.values(): 104 | if i in self.selections: 105 | indexes.append(index) 106 | index +=1 107 | return tuple(indexes) 108 | 109 | else: 110 | for i in self.buttons.values(): 111 | if i==self.selected: 112 | return index 113 | else: 114 | index +=1 115 | 116 | def bind(self, key, func): 117 | super().bind_all(key, lambda e: func(self.get()), add="+") 118 | 119 | def deselect(self, index): 120 | if not self.multiple: 121 | self.selected.configure(fg_color="transparent") 122 | self.selected = None 123 | return 124 | if self.buttons[index] in self.selections: 125 | self.selections.remove(self.buttons[index]) 126 | self.buttons[index].configure(fg_color="transparent") 127 | 128 | def deactivate(self, index): 129 | if str(index).lower()=="all": 130 | for i in self.buttons: 131 | self.deselect(i) 132 | return 133 | selected = list(self.buttons.keys())[index] 134 | self.deselect(selected) 135 | 136 | def insert(self, index, option, keybind=None ,func=None, **args): 137 | """ add new option in the listbox """ 138 | 139 | if str(index).lower()=="end": 140 | index = f"END{self.end_num}" 141 | self.end_num +=1 142 | 143 | if index in self.buttons: 144 | self.buttons[index].destroy() 145 | 146 | self.buttons[index] = customtkinter.CTkButton(self, text=option, fg_color="transparent", anchor=self.justify, 147 | text_color=self.text_color, font=self.font, 148 | hover_color=self.hover_color, **args) 149 | self.buttons[index].configure(command=lambda num=index: self.select(num)) 150 | self.buttons[index].pack(padx=0, pady=(0,5), fill="x", expand=True) 151 | 152 | if keybind: 153 | self.buttons[index].bind(keybind, func) 154 | 155 | def delete(self, index, last=None): 156 | """ delete options from the listbox """ 157 | 158 | if str(index).lower()=="all": 159 | for i in self.buttons: 160 | self.buttons[i].destroy() 161 | self.buttons = {} 162 | self.end_num = 0 163 | return 164 | 165 | if str(index).lower()=="end": 166 | index = f"END{self.end_num}" 167 | self.end_num -=1 168 | else: 169 | if int(index)==len(self.buttons): 170 | index = len(self.buttons)-1 171 | if int(index)>len(self.buttons): 172 | return 173 | if not last: 174 | index = list(self.buttons.keys())[int(index)] 175 | 176 | if last: 177 | if str(last).lower()=="end": 178 | last = len(self.buttons)-1 179 | elif int(last)>=len(self.buttons): 180 | last = len(self.buttons)-1 181 | 182 | deleted_list = [] 183 | for i in range(index, int(last)+1): 184 | list(self.buttons.values())[i].destroy() 185 | deleted_list.append(list(self.buttons.keys())[i]) 186 | for i in deleted_list: 187 | del self.buttons[i] 188 | else: 189 | self.buttons[index].destroy() 190 | del self.buttons[index] 191 | 192 | def size(self): 193 | """ return total number of items in the listbox """ 194 | return len(self.buttons.keys()) 195 | 196 | def get(self, index=None): 197 | """ get the selected value """ 198 | if index: 199 | if str(index).lower()=="all": 200 | return list(item.cget("text") for item in self.buttons.values()) 201 | else: 202 | index = list(self.buttons.keys())[int(index)] 203 | return self.buttons[index].cget("text") 204 | else: 205 | if self.multiple: 206 | return [x.cget("text") for x in self.selections] if len(self.selections)>0 else None 207 | else: 208 | return self.selected.cget("text") if self.selected is not None else None 209 | 210 | def configure(self, **kwargs): 211 | """ configurable options of the listbox """ 212 | 213 | if "hover_color" in kwargs: 214 | self.hover_color = kwargs.pop("hover_color") 215 | for i in self.buttons.values(): 216 | i.configure(hover_color=self.hover_color) 217 | if "highlight_color" in kwargs: 218 | self.select_color = kwargs.pop("highlight_color") 219 | if self.selected: self.selected.configure(fg_color=self.select_color) 220 | if len(self.selections)>0: 221 | for i in self.selections: 222 | i.configure(fg_color=self.select_color) 223 | if "text_color" in kwargs: 224 | self.text_color = kwargs.pop("text_color") 225 | for i in self.buttons.values(): 226 | i.configure(text=self.text_color) 227 | if "font" in kwargs: 228 | self.font = kwargs.pop("font") 229 | for i in self.buttons.values(): 230 | i.configure(font=self.font) 231 | if "command" in kwargs: 232 | self.command = kwargs.pop("command") 233 | 234 | super().configure(**kwargs) 235 | -------------------------------------------------------------------------------- /boiiiwd_package/src/CTkToolTip/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CustomTkinter ToolTip 3 | Author: Akash Bora 4 | This is a tooltip/pop-up widget made with customtkinter. 5 | Homepage: https://github.com/Akascape/CTkToolTip 6 | """ 7 | 8 | __version__ = '0.8' 9 | 10 | from .ctk_tooltip import CTkToolTip 11 | -------------------------------------------------------------------------------- /boiiiwd_package/src/CTkToolTip/ctk_tooltip.py: -------------------------------------------------------------------------------- 1 | """ 2 | CTkToolTip Widget 3 | version: 0.8 4 | """ 5 | 6 | import time 7 | import sys 8 | import customtkinter 9 | import threading 10 | from tkinter import Toplevel, Frame 11 | 12 | class CTkToolTip(Toplevel): 13 | """ 14 | Creates a ToolTip (pop-up) widget for customtkinter. 15 | """ 16 | 17 | def __init__( 18 | self, 19 | widget: any = None, 20 | message: str = None, 21 | delay: float = 0.2, 22 | follow: bool = True, 23 | x_offset: int = +20, 24 | y_offset: int = +10, 25 | bg_color: str = None, 26 | corner_radius: int = 10, 27 | border_width: int = 0, 28 | border_color: str = None, 29 | alpha: float = 0.95, 30 | padding: tuple = (10, 2), 31 | topmost: bool = False, 32 | is_noti: bool = False, 33 | noti_event: any = None, 34 | noti_dur: float = 3.0, 35 | **message_kwargs): 36 | 37 | super().__init__() 38 | 39 | self.widget = widget 40 | 41 | if not is_noti: 42 | self.withdraw() 43 | 44 | if topmost: 45 | self.attributes('-topmost', 'true') 46 | 47 | # Disable ToolTip's title bar 48 | self.overrideredirect(True) 49 | 50 | if sys.platform.startswith("win"): 51 | self.transparent_color = self.widget._apply_appearance_mode( 52 | customtkinter.ThemeManager.theme["CTkToplevel"]["fg_color"]) 53 | self.attributes("-transparentcolor", self.transparent_color) 54 | self.transient() 55 | elif sys.platform.startswith("darwin"): 56 | self.transparent_color = 'systemTransparent' 57 | self.attributes("-transparent", True) 58 | self.transient(self.master) 59 | else: 60 | self.transparent_color = '#000001' 61 | corner_radius = 0 62 | self.transient() 63 | 64 | self.resizable(width=True, height=True) 65 | 66 | # Make the background transparent 67 | self.config(background=self.transparent_color) 68 | 69 | # StringVar instance for msg string 70 | self.messageVar = customtkinter.StringVar() 71 | self.message = message 72 | self.messageVar.set(self.message) 73 | 74 | self.noti_dur = noti_dur 75 | self.is_noti = is_noti 76 | self.delay = delay 77 | self.follow = follow 78 | self.x_offset = x_offset 79 | self.y_offset = y_offset 80 | self.corner_radius = corner_radius 81 | self.alpha = alpha 82 | self.border_width = border_width 83 | self.padding = padding 84 | self.bg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if bg_color is None else bg_color 85 | self.border_color = border_color 86 | self.disable = False 87 | 88 | # visibility status of the ToolTip inside|outside|visible 89 | self.status = "outside" 90 | self.last_moved = 0 91 | self.attributes('-alpha', self.alpha) 92 | 93 | if sys.platform.startswith("win"): 94 | if self.widget._apply_appearance_mode(self.bg_color) == self.transparent_color: 95 | self.transparent_color = "#000001" 96 | self.config(background=self.transparent_color) 97 | self.attributes("-transparentcolor", self.transparent_color) 98 | 99 | # Add the message widget inside the tooltip 100 | self.transparent_frame = Frame(self, bg=self.transparent_color) 101 | self.transparent_frame.pack(padx=0, pady=0, fill="both", expand=True) 102 | 103 | self.frame = customtkinter.CTkFrame(self.transparent_frame, bg_color=self.transparent_color, 104 | corner_radius=self.corner_radius, 105 | border_width=self.border_width, fg_color=self.bg_color, 106 | border_color=self.border_color) 107 | self.frame.pack(padx=0, pady=0, fill="both", expand=True) 108 | 109 | self.message_label = customtkinter.CTkLabel(self.frame, textvariable=self.messageVar, **message_kwargs) 110 | self.message_label.pack(fill="both", padx=self.padding[0] + self.border_width, 111 | pady=self.padding[1] + self.border_width, expand=True) 112 | 113 | if self.widget.winfo_name() != "tk": 114 | if self.frame.cget("fg_color") == self.widget.cget("bg_color"): 115 | if not bg_color: 116 | self._top_fg_color = self.frame._apply_appearance_mode( 117 | customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"]) 118 | if self._top_fg_color != self.transparent_color: 119 | self.frame.configure(fg_color=self._top_fg_color) 120 | 121 | # Add bindings to the widget without overriding the existing ones 122 | if self.is_noti: 123 | if noti_event: 124 | self.on_enter(noti_event) 125 | else: 126 | self.widget.bind("", self.on_enter, add="+") 127 | else: 128 | self.widget.bind("", self.on_enter, add="+") 129 | self.widget.bind("", self.on_leave, add="+") 130 | self.widget.bind("", self.on_enter, add="+") 131 | self.widget.bind("", self.on_enter, add="+") 132 | self.widget.bind("", lambda _: self.hide(), add="+") 133 | 134 | def show(self) -> None: 135 | """ 136 | Enable the widget. 137 | """ 138 | self.disable = False 139 | 140 | def on_enter(self, event) -> None: 141 | """ 142 | Processes motion within the widget including entering and moving. 143 | """ 144 | 145 | if self.disable: 146 | return 147 | self.last_moved = time.time() 148 | 149 | # Set the status as inside for the very first time 150 | if self.status == "outside": 151 | self.status = "inside" 152 | 153 | # If the follow flag is not set, motion within the widget will make the ToolTip dissapear 154 | if not self.follow: 155 | self.status = "inside" 156 | self.withdraw() 157 | 158 | # Calculate available space on the right side of the widget relative to the screen 159 | root_width = self.winfo_screenwidth() 160 | widget_x = event.x_root 161 | space_on_right = root_width - widget_x 162 | 163 | # Calculate the width of the tooltip's text based on the length of the message string 164 | text_width = self.message_label.winfo_reqwidth() 165 | 166 | # Calculate the offset based on available space and text width to avoid going off-screen on the right side 167 | offset_x = self.x_offset 168 | if space_on_right < text_width + 20: # Adjust the threshold as needed 169 | offset_x = -text_width - 20 # Negative offset when space is limited on the right side 170 | 171 | # Offsets the ToolTip using the coordinates od an event as an origin 172 | self.geometry(f"+{event.x_root + offset_x}+{event.y_root + self.y_offset}") 173 | 174 | # Time is in integer: milliseconds 175 | self.after(int(self.delay * 1000), self._show) 176 | 177 | if self.is_noti: 178 | threading.Timer(self.noti_dur, self.destroy).start() 179 | 180 | def on_leave(self, event=None) -> None: 181 | """ 182 | Hides the ToolTip temporarily. 183 | """ 184 | 185 | if self.disable: return 186 | self.status = "outside" 187 | self.withdraw() 188 | 189 | def _show(self) -> None: 190 | """ 191 | Displays the ToolTip. 192 | """ 193 | 194 | if not self.widget.winfo_exists(): 195 | self.hide() 196 | self.destroy() 197 | 198 | if self.status == "inside" and time.time() - self.last_moved >= self.delay: 199 | self.status = "visible" 200 | self.deiconify() 201 | 202 | def hide(self) -> None: 203 | """ 204 | Disable the widget from appearing. 205 | """ 206 | if not self.winfo_exists(): 207 | return 208 | self.withdraw() 209 | self.disable = True 210 | 211 | def is_disabled(self) -> None: 212 | """ 213 | Return the window state 214 | """ 215 | return self.disable 216 | 217 | def get(self) -> None: 218 | """ 219 | Returns the text on the tooltip. 220 | """ 221 | return self.messageVar.get() 222 | 223 | def configure(self, message: str = None, delay: float = None, bg_color: str = None, **kwargs): 224 | """ 225 | Set new message or configure the label parameters. 226 | """ 227 | if delay: self.delay = delay 228 | if bg_color: self.frame.configure(fg_color=bg_color) 229 | 230 | self.messageVar.set(message) 231 | self.message_label.configure(**kwargs) 232 | -------------------------------------------------------------------------------- /boiiiwd_package/src/__init__.py: -------------------------------------------------------------------------------- 1 | import os, glob 2 | from os.path import dirname, basename, isfile, join 3 | 4 | modules = glob.glob(os.path.join(dirname(__file__), '*.py')) 5 | __all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] -------------------------------------------------------------------------------- /boiiiwd_package/src/helpers.py: -------------------------------------------------------------------------------- 1 | import src.shared_vars as main_app 2 | from src.imports import * 3 | 4 | # Start helper functions 5 | 6 | # testing app offline 7 | # import socket 8 | # def guard(*args, **kwargs): 9 | # pass 10 | # socket.socket = guard 11 | 12 | 13 | def check_config(name, fallback=None): 14 | config = configparser.ConfigParser() 15 | config.read(CONFIG_FILE_PATH) 16 | if fallback: 17 | return config.get("Settings", name, fallback=fallback) 18 | return config.get("Settings", name, fallback="") 19 | 20 | 21 | def save_config(name, value): 22 | config = configparser.ConfigParser() 23 | config.read(CONFIG_FILE_PATH) 24 | if name and value: 25 | config.set("Settings", name, value) 26 | with open(CONFIG_FILE_PATH, "w", encoding='utf-8', errors="ignore") as config_file: 27 | config.write(config_file) 28 | 29 | 30 | def check_custom_theme(theme_name): 31 | if os.path.exists(os.path.join(APPLICATION_PATH, theme_name)): 32 | return os.path.join(APPLICATION_PATH, theme_name) 33 | else: 34 | try: 35 | return os.path.join(RESOURCES_DIR, theme_name) 36 | except: 37 | return os.path.join(RESOURCES_DIR, "boiiiwd_theme.json") 38 | 39 | 40 | # theme initialization 41 | # Modes: "System" (standard), "Dark", "Light" 42 | ctk.set_appearance_mode(check_config("appearance", "Dark")) 43 | try: 44 | ctk.set_default_color_theme(check_custom_theme( 45 | check_config("theme", fallback="boiiiwd_theme.json"))) 46 | except: 47 | save_config("theme", "boiiiwd_theme.json") 48 | ctk.set_default_color_theme(os.path.join( 49 | RESOURCES_DIR, "boiiiwd_theme.json")) 50 | 51 | 52 | def get_latest_release_version(): 53 | try: 54 | release_api_url = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest" 55 | response = requests.get(release_api_url) 56 | response.raise_for_status() 57 | data = response.json() 58 | return data["tag_name"] 59 | except requests.exceptions.RequestException as e: 60 | show_message("Warning", f"Error while checking for updates: \n{e}") 61 | return None 62 | 63 | 64 | def create_update_script(current_exe, new_exe, updater_folder, program_name): 65 | script_content = f""" 66 | @echo off 67 | echo Terminating BOIIIWD.exe... 68 | taskkill /im "{program_name}" /t /f 69 | 70 | echo Replacing BOIIIWD.exe... 71 | cd "{updater_folder}" 72 | taskkill /im "{program_name}" /t /f 73 | move /y "{new_exe}" "../"{program_name}"" 74 | 75 | echo Starting BOIIIWD.exe... 76 | cd .. 77 | start "" "{current_exe}" 78 | 79 | echo Exiting! 80 | exit 81 | """ 82 | 83 | script_path = os.path.join(updater_folder, "boiiiwd_updater.bat") 84 | with open(script_path, "w", encoding='utf-8', errors="ignore") as script_file: 85 | script_file.write(script_content) 86 | 87 | return script_path 88 | 89 | 90 | def if_internet_available(func=None, launching=False): 91 | def check_internet(): 92 | try: 93 | socket.create_connection(("8.8.8.8", 53), timeout=3) 94 | return True 95 | except: 96 | return False 97 | 98 | if func == "return": 99 | return check_internet() 100 | 101 | def wrapper(*args, **kwargs): 102 | if check_internet(): 103 | return func(*args, **kwargs) 104 | else: 105 | if not launching: 106 | show_message( 107 | "Offline", "No internet connection. Please check your internet connection and try again.") 108 | return 109 | 110 | return wrapper 111 | 112 | 113 | 114 | @if_internet_available 115 | def check_for_updates_func(window, ignore_up_todate=False): 116 | print(f'[{get_current_datetime()}] [Logs] check_for_updates_func invoked...') 117 | try: 118 | latest_version = get_latest_release_version() 119 | current_version = VERSION 120 | int_latest_version = int( 121 | latest_version.replace("v", "").replace(".", "")) 122 | int_current_version = int( 123 | current_version.replace("v", "").replace(".", "")) 124 | 125 | if latest_version and int_latest_version > int_current_version: 126 | 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) 127 | 128 | result = msg_box.get() 129 | 130 | if result == "View": 131 | webbrowser.open( 132 | f"https://github.com/{GITHUB_REPO}/releases/latest") 133 | 134 | if result == "Yes": 135 | from src.update_window import UpdateWindow 136 | update_window = UpdateWindow(window, LATEST_RELEASE_URL) 137 | update_window.start_update() 138 | 139 | if result == "No": 140 | return 141 | 142 | elif int_latest_version < int_current_version: 143 | if ignore_up_todate: 144 | return 145 | msg_box = CTkMessagebox(title="Up to Date!", message=f"Unreleased version!\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="Ok", sound=True) 146 | result = msg_box.get() 147 | elif int_latest_version == int_current_version: 148 | if ignore_up_todate: 149 | return 150 | msg_box = CTkMessagebox( 151 | title="Up to Date!", message="No Updates Available!", option_1="Ok", sound=True) 152 | result = msg_box.get() 153 | 154 | else: 155 | show_message( 156 | "Error!", "An error occured while checking for updates!\nCheck your internet and try again") 157 | 158 | except Exception as e: 159 | print(f"[{get_current_datetime()}] [logs] error in check_for_updates_func: {e}") 160 | show_message("Error", f"Error while checking for updates: \n{e}", icon="cancel") 161 | 162 | 163 | def extract_workshop_id(link): 164 | try: 165 | pattern = r'(?<=id=)(\d+)' 166 | match = re.search(pattern, link) 167 | 168 | if match: 169 | return match.group(0) 170 | else: 171 | return None 172 | except: 173 | return None 174 | 175 | 176 | def check_steamcmd(): 177 | steamcmd_path = get_steamcmd_path() 178 | steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe") 179 | 180 | if not os.path.exists(steamcmd_exe_path): 181 | return False 182 | 183 | return True 184 | 185 | 186 | def initialize_steam(master): 187 | print(f'[{get_current_datetime()}] [Logs] initialize_steam invoked...') 188 | try: 189 | steamcmd_path = get_steamcmd_path() 190 | steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe") 191 | process = subprocess.Popen( 192 | [steamcmd_exe_path, "+quit"], creationflags=subprocess.CREATE_NEW_CONSOLE) 193 | master.attributes('-alpha', 0.0) 194 | process.wait() 195 | if is_steamcmd_initialized(): 196 | show_message("SteamCMD has terminated!", 197 | "BOIIIWD is ready for action.", icon="info") 198 | else: 199 | show_message("SteamCMD has terminated!!", 200 | "SteamCMD isn't initialized yet") 201 | except: 202 | show_message( 203 | "Error!", "An error occurred please check your paths and try again.", icon="cancel") 204 | master.attributes('-alpha', 1.0) 205 | 206 | 207 | @if_internet_available 208 | def valid_id(workshop_id): 209 | data = item_steam_api(workshop_id) 210 | if check_config("skip_invalid", "on") == "off": 211 | if data: 212 | return True 213 | if "consumer_app_id" in data['response']['publishedfiledetails'][0]: 214 | return True 215 | else: 216 | return False 217 | 218 | 219 | def convert_speed(speed_bytes): 220 | if speed_bytes < 1024: 221 | return speed_bytes, "B/s" 222 | elif speed_bytes < 1024 * 1024: 223 | return speed_bytes / 1024, "KB/s" 224 | elif speed_bytes < 1024 * 1024 * 1024: 225 | return speed_bytes / (1024 * 1024), "MB/s" 226 | else: 227 | return speed_bytes / (1024 * 1024 * 1024), "GB/s" 228 | 229 | 230 | def create_default_config(): 231 | config = configparser.ConfigParser() 232 | config["Settings"] = { 233 | "SteamCMDPath": APPLICATION_PATH, 234 | "DestinationFolder": "", 235 | "checkforupdates": "on", 236 | "console": "off" 237 | } 238 | with open(CONFIG_FILE_PATH, "w", encoding='utf-8', errors="ignore") as config_file: 239 | config.write(config_file) 240 | 241 | 242 | def get_steamcmd_path(): 243 | config = configparser.ConfigParser() 244 | config.read(CONFIG_FILE_PATH) 245 | return config.get("Settings", "SteamCMDPath", fallback=APPLICATION_PATH) 246 | 247 | 248 | def extract_json_data(json_path, key): 249 | with open(json_path, 'r', encoding='utf-8', errors="ignore") as json_file: 250 | data = json.load(json_file) 251 | return data.get(key, '') 252 | 253 | 254 | def convert_bytes_to_readable(size_in_bytes, no_symb=None): 255 | for unit in ['B', 'KB', 'MB', 'GB', 'TB']: 256 | if size_in_bytes < 1024.0: 257 | if no_symb: 258 | return f"{size_in_bytes:.2f}" 259 | return f"{size_in_bytes:.2f} {unit}" 260 | size_in_bytes /= 1024.0 261 | 262 | 263 | def get_workshop_file_size(workshop_id, raw=None): 264 | print(f'[{get_current_datetime()}] [Logs] get_workshop_file_size invoked...') 265 | url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}&searchtext=" 266 | response = requests.get(url) 267 | soup = BeautifulSoup(response.text, "html.parser") 268 | file_size_element = soup.find("div", class_="detailsStatRight") 269 | 270 | if not file_size_element: 271 | return None 272 | 273 | file_size_text = file_size_element.get_text(strip=True).replace(",", "") 274 | 275 | try: 276 | if "GB" in file_size_text: 277 | file_size_in_gb = float(file_size_text.replace(" GB", "")) 278 | file_size_in_bytes = int(file_size_in_gb * 1024 * 1024 * 1024) 279 | elif "MB" in file_size_text: 280 | file_size_in_mb = float(file_size_text.replace(" MB", "")) 281 | file_size_in_bytes = int(file_size_in_mb * 1024 * 1024) 282 | elif "KB" in file_size_text: 283 | file_size_in_kb = float(file_size_text.replace(" KB", "")) 284 | file_size_in_bytes = int(file_size_in_kb * 1024) 285 | elif "B" in file_size_text: 286 | file_size_in_b = float(file_size_text.replace(" B", "")) 287 | file_size_in_bytes = int(file_size_in_b) 288 | else: 289 | raise ValueError(f"Unsupported file size format: {file_size_text}") 290 | 291 | if raw: 292 | return convert_bytes_to_readable(file_size_in_bytes) 293 | return file_size_in_bytes 294 | 295 | except Exception as e: 296 | print(f"[{get_current_datetime()}] [logs] error in get_workshop_file_size: {e}") 297 | return None 298 | 299 | 300 | def show_message(title, message, icon="warning", _return=False, option_1="No", option_2="Ok"): 301 | if _return: 302 | msg = CTkMessagebox(title=title, message=message, icon=icon, 303 | option_1=option_1, option_2=option_2, sound=True) 304 | response = msg.get() 305 | if response == option_1: 306 | return False 307 | elif response == option_2: 308 | return True 309 | else: 310 | return False 311 | else: 312 | def callback(): 313 | CTkMessagebox(title=title, message=message, icon=icon, sound=True) 314 | main_app.app.after(0, callback) 315 | 316 | 317 | def input_popup(title="Input", message="Enter value:"): 318 | try: 319 | dialog = ctk.CTkInputDialog(text=message, title=title) 320 | return dialog.get_input() 321 | except Exception as e: 322 | print(f"[{get_current_datetime()}] [logs] error in input_popup: {e}") 323 | 324 | def launch_game_func(path, procname="BlackOps3.exe", flags=""): 325 | print(f'[{get_current_datetime()}] [Logs] launch_game_func invoked...') 326 | if not procname.endswith(('.exe', '.bat', '.sh', '.lnk')): 327 | procname += '.exe' 328 | try: 329 | if procname in (p.name() for p in psutil.process_iter()): 330 | for proc in psutil.process_iter(): 331 | if proc.name() == procname: 332 | proc.kill() 333 | game_path = os.path.join(path, procname) 334 | subprocess.Popen([game_path, flags], cwd=path) 335 | show_message( 336 | "Please wait!", "The game has launched in the background it will open up in a sec!", icon="info") 337 | except Exception as e: 338 | print(f"[{get_current_datetime()}] [logs] error in launch_game_func: {e}") 339 | show_message(f"Error: Failed to launch game", f"Failed to start {procname}\n\nMake sure the game path is correct.\n\n{e}") 340 | 341 | 342 | def remove_tree(folder_path, show_error=None): 343 | if show_error: 344 | try: 345 | shutil.rmtree(folder_path) 346 | except Exception as e: 347 | print(f"[{get_current_datetime()}] [logs] error in remove_tree: {e}") 348 | show_message("Error!", f"An error occurred while trying to remove files:\n{e}", icon="cancel") 349 | try: 350 | shutil.rmtree(folder_path) 351 | except Exception as e: 352 | pass 353 | 354 | 355 | def convert_seconds(seconds): 356 | minutes, seconds = divmod(seconds, 60) 357 | hours, minutes = divmod(minutes, 60) 358 | return hours, minutes, seconds 359 | 360 | 361 | def get_folder_size(folder_path): 362 | total_size = 0 363 | for path, dirs, files in os.walk(folder_path): 364 | for f in files: 365 | fp = os.path.join(path, f) 366 | total_size += os.stat(fp).st_size 367 | return total_size 368 | 369 | 370 | def is_steamcmd_initialized(): 371 | steamcmd_path = get_steamcmd_path() 372 | steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe") 373 | steamcmd_size = os.path.getsize(steamcmd_exe_path) 374 | if steamcmd_size < 3 * 1024 * 1024: 375 | return False 376 | return True 377 | 378 | 379 | def get_button_state_colors(file_path, state): 380 | try: 381 | with open(file_path, 'r', encoding='utf-8', errors="ignore") as json_file: 382 | data = json.load(json_file) 383 | if 'BOIIIWD_Globals' in data: 384 | boiiiwd_globals = data['BOIIIWD_Globals'] 385 | if state in boiiiwd_globals: 386 | return boiiiwd_globals[state] 387 | else: 388 | return None 389 | else: 390 | return None 391 | except FileNotFoundError: 392 | return None 393 | except json.JSONDecodeError: 394 | return None 395 | 396 | 397 | def reset_steamcmd(no_warn=None): 398 | print(f'[{get_current_datetime()}] [Logs] reset_steamcmd invoked...') 399 | steamcmd_path = get_steamcmd_path() 400 | 401 | directories_to_reset = ["steamapps", "dumps", 402 | "logs", "depotcache", "appcache", "userdata",] 403 | 404 | for directory in directories_to_reset: 405 | directory_path = os.path.join(steamcmd_path, directory) 406 | if os.path.exists(directory_path): 407 | remove_tree(directory_path, show_error=True) 408 | 409 | for root, _, files in os.walk(steamcmd_path): 410 | for filename in files: 411 | if filename.endswith((".old", ".crash")): 412 | file_path = os.path.join(root, filename) 413 | os.remove(file_path) 414 | 415 | if not no_warn: 416 | show_message( 417 | "Success!", "SteamCMD has been reset successfully!", icon="info") 418 | else: 419 | if not no_warn: 420 | show_message( 421 | "Warning!", "steamapps folder was not found, maybe already removed?", icon="warning") 422 | 423 | 424 | def get_item_name(id): 425 | try: 426 | data = item_steam_api(id) 427 | try: 428 | name = data['response']['publishedfiledetails'][0]['title'] 429 | return name 430 | except: 431 | return True 432 | except: 433 | return False 434 | 435 | # you gotta use my modded CTkToolTip originaly by Akascape 436 | 437 | 438 | def show_noti(widget, message, event=None, noti_dur=3.0, topmost=False): 439 | CTkToolTip(widget, message=message, is_noti=True, 440 | noti_event=event, noti_dur=noti_dur, topmost=topmost) 441 | 442 | 443 | def get_update_time_from_html(workshop_id): 444 | current_year = datetime.now().year 445 | date_format_with_year = "%d %b, %Y @ %I:%M%p" 446 | date_format_with_added_year = "%d %b @ %I:%M%p, %Y" 447 | url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}" 448 | 449 | try: 450 | response = requests.get(url) 451 | response.raise_for_status() 452 | content = response.text 453 | 454 | soup = BeautifulSoup(content, "html.parser") 455 | 456 | details_stats_container = soup.find( 457 | "div", class_="detailsStatsContainerRight") 458 | if details_stats_container: 459 | details_stat_elements = details_stats_container.find_all( 460 | "div", class_="detailsStatRight") 461 | try: 462 | date_updated = details_stat_elements[2].text.strip() 463 | except: 464 | date_updated = None 465 | 466 | if not date_updated: 467 | return None 468 | 469 | try: 470 | date_updated = datetime.strptime( 471 | date_updated, date_format_with_year) 472 | except ValueError: 473 | date_updated = datetime.strptime( 474 | date_updated + f", {current_year}", date_format_with_added_year) 475 | 476 | return int(date_updated.timestamp()) 477 | 478 | except Exception as e: 479 | print(f"Error getting update time for URL {url}: {e}") 480 | return None 481 | 482 | 483 | def check_item_date(down_date, date_updated, format=False): 484 | current_year = datetime.now().year 485 | date_format_with_year = "%d %b, %Y @ %I:%M%p" 486 | date_format_with_added_year = "%d %b @ %I:%M%p, %Y" 487 | try: 488 | try: 489 | download_datetime = datetime.strptime( 490 | down_date, date_format_with_year) 491 | except ValueError: 492 | download_datetime = datetime.strptime( 493 | down_date + f", {current_year}", date_format_with_added_year) 494 | 495 | if format: 496 | try: 497 | date_updated = datetime.strptime( 498 | date_updated, date_format_with_year) 499 | except ValueError: 500 | date_updated = datetime.strptime( 501 | date_updated + f", {current_year}", date_format_with_added_year) 502 | 503 | if date_updated >= download_datetime: 504 | return True 505 | elif date_updated < download_datetime: 506 | return False 507 | except: 508 | return False 509 | 510 | 511 | def get_window_size_from_registry(): 512 | try: 513 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, REGISTRY_KEY_PATH, 0, winreg.KEY_READ) as key: 514 | width, _ = winreg.QueryValueEx(key, "WindowWidth") 515 | height, _ = winreg.QueryValueEx(key, "WindowHeight") 516 | x, _ = winreg.QueryValueEx(key, "WindowX") 517 | y, _ = winreg.QueryValueEx(key, "WindowY") 518 | return int(width), int(height), int(x), int(y) 519 | except (FileNotFoundError, OSError, ValueError, FileNotFoundError): 520 | return None, None, None, None 521 | 522 | 523 | def save_window_size_to_registry(width, height, x, y): 524 | try: 525 | with winreg.CreateKey(winreg.HKEY_CURRENT_USER, REGISTRY_KEY_PATH) as key: 526 | winreg.SetValueEx(key, "WindowWidth", 0, winreg.REG_SZ, str(width)) 527 | winreg.SetValueEx(key, "WindowHeight", 0, 528 | winreg.REG_SZ, str(height)) 529 | winreg.SetValueEx(key, "WindowX", 0, winreg.REG_SZ, str(x)) 530 | winreg.SetValueEx(key, "WindowY", 0, winreg.REG_SZ, str(y)) 531 | except Exception as e: 532 | print(f"Error saving to registry: {e}") 533 | 534 | 535 | def item_steam_api(id): 536 | try: 537 | url = ITEM_INFO_API 538 | data = { 539 | "itemcount": 1, 540 | "publishedfileids[0]": int(id), 541 | } 542 | info = requests.post(url, data=data) 543 | return info.json() 544 | 545 | except Exception as e: 546 | print(e) 547 | return False 548 | 549 | 550 | def get_item_dates(ids): 551 | try: 552 | data = { 553 | "itemcount": len(ids), 554 | } 555 | for i, id in enumerate(ids): 556 | data[f"publishedfileids[{i}]"] = int(id) 557 | 558 | info = requests.post(ITEM_INFO_API, data=data) 559 | response_data = info.json() 560 | 561 | if "response" in response_data: 562 | item_details = response_data["response"]["publishedfiledetails"] 563 | return_list = {item["publishedfileid"]: item.get( 564 | "time_updated", None) for item in item_details} 565 | return return_list 566 | 567 | return {} 568 | 569 | except Exception as e: 570 | print("Error: could not fetch all update times. Breaking early.") 571 | return {} 572 | 573 | 574 | def isNullOrWhiteSpace(str): 575 | if (str is None) or (str == "") or (str.isspace()): 576 | return True 577 | return False 578 | 579 | 580 | def concatenate_sublists(a): 581 | out = [] 582 | for sublist in a: 583 | out.extend(sublist) 584 | return out 585 | 586 | 587 | def nextnonexistentdir(f, dir=os.path.dirname(os.path.realpath(__file__))): 588 | i = 0 589 | 590 | def already_exists(i): 591 | if i==0 and os.path.exists(os.path.join(dir, (f).strip())): 592 | return True 593 | else: 594 | return os.path.exists(os.path.join(dir, (f + f'_{str(i)}').strip())) 595 | 596 | while already_exists(i): 597 | i += 1 598 | 599 | root_i_ext = [f, i] 600 | 601 | return root_i_ext 602 | 603 | 604 | def xor_encrypt_decrypt(data, key): 605 | key_len = len(key) 606 | result = bytearray() 607 | 608 | for i in range(len(data)): 609 | result.append(data[i] ^ key[i % key_len]) 610 | 611 | return bytes(result) 612 | 613 | 614 | def obfuscate(data): 615 | try: 616 | data_bytes = data.encode('utf-8') 617 | encrypted_data = xor_encrypt_decrypt(data_bytes, BOIIIWD_ENC_KEY) 618 | return base64.b64encode(encrypted_data).decode('utf-8') 619 | except Exception as e: 620 | print(f"Encryption error: {e}") 621 | return "" 622 | 623 | 624 | def unobfuscate(data): 625 | try: 626 | encrypted_data = base64.b64decode(data) 627 | decrypted_data = xor_encrypt_decrypt(encrypted_data, BOIIIWD_ENC_KEY) 628 | return decrypted_data.decode('utf-8') 629 | except Exception as e: 630 | print(f"Decryption error: {e}") 631 | return "" 632 | 633 | 634 | def save_steam_creds(steam_username): 635 | save_config("13ead2e5e894dd32839df1d494056f7c", obfuscate(steam_username)) 636 | 637 | 638 | def load_steam_creds(): 639 | user = unobfuscate(check_config("13ead2e5e894dd32839df1d494056f7c", "")) 640 | return user 641 | 642 | 643 | def invalid_password_check(stdout_text: str) -> str | bool: 644 | if stdout_text: 645 | try: 646 | return_error_messages = [ 647 | "FAILED (Invalid Password)", # 0 648 | "FAILED (Rate Limit Exceeded)", # 1 649 | "FAILED (Two-factor code mismatch)", # 2 650 | "FAILED (Invalid Login Auth Code)", # 3 651 | "Invalid Password", # 4 652 | "FAILED", # 5, 653 | "password:", # 6 654 | "Two-factor code:" #7 655 | ] 656 | 657 | for message in return_error_messages: 658 | if message in stdout_text: 659 | save_config("login_cached", "off") 660 | if message == (return_error_messages[6] or return_error_messages[7]): 661 | return message + " - Password prompt detected, Cashed login is now off, try again!" 662 | elif message == return_error_messages[1]: 663 | return message + " - Rate limit exceeded, try again later!" 664 | return message + " - Cashed login is now off, try again!" 665 | 666 | return False 667 | 668 | except Exception as e: 669 | print(f"Error in invalid_password_check: {e}") 670 | return False 671 | else: 672 | return False 673 | 674 | 675 | # will be reworked in the future 676 | def initiate_login_process(command, console): 677 | print(f'[{get_current_datetime()}] [Logs] initiate_login_process invoked...') 678 | try: 679 | if console: 680 | hide_console() 681 | path = command.split('+login')[0].strip() 682 | username = command.split("+login")[1].strip().split(" ")[0] 683 | print(f"[{get_current_datetime()}] [Logs] Initializing login process for {username}...") 684 | final_cmd = f'start /wait cmd /c "{path} +login {username}"' 685 | subprocess.run(final_cmd, shell=True) 686 | save_config("login_cached", "on") 687 | if console: 688 | show_console() 689 | return True 690 | except Exception as e: 691 | if console: 692 | show_console() 693 | print(f"Error running command in new window: {e}") 694 | return False 695 | 696 | 697 | def show_console(): 698 | ctypes.windll.kernel32.AllocConsole() 699 | new_console_handle = ctypes.windll.kernel32.GetStdHandle(ctypes.c_uint(-11)) 700 | new_console_fd = os.open('CONOUT$', os.O_RDWR | os.O_TEXT) 701 | sys.stdout = os.fdopen(new_console_fd, 'w') 702 | sys.stderr = sys.stdout 703 | 704 | 705 | def hide_console(): 706 | hwnd = ctypes.windll.kernel32.GetConsoleWindow() 707 | 708 | if hwnd: 709 | ctypes.windll.user32.ShowWindow(hwnd, 0) 710 | ctypes.windll.kernel32.FreeConsole() 711 | 712 | sys.stdout = sys.__stdout__ 713 | sys.stderr = sys.__stderr__ 714 | 715 | 716 | def get_current_datetime(): 717 | try: 718 | return datetime.now().strftime('%Y-%m-%d %H:%M:%S') 719 | except Exception as e: 720 | print(f"[Logs] Error in get_current_datetime: {e}") 721 | return "" 722 | 723 | def kill_steamcmd(): 724 | try: subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], creationflags=subprocess.CREATE_NO_WINDOW) 725 | except Exception as e: print(f'[{get_current_datetime()}] [Logs] Error in kill_steamcmd: {e}') 726 | 727 | # End helper functions 728 | -------------------------------------------------------------------------------- /boiiiwd_package/src/imports.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import io 3 | import math 4 | import os 5 | import re 6 | import shutil 7 | import subprocess 8 | import sys 9 | import threading 10 | import time 11 | import webbrowser 12 | import base64 13 | import zipfile 14 | 15 | from datetime import datetime 16 | from pathlib import Path 17 | from tkinter import END, Event, Menu 18 | 19 | import customtkinter as ctk 20 | import ujson as json 21 | import psutil 22 | import requests 23 | import socket 24 | import winreg 25 | import ctypes 26 | import shlex 27 | from bs4 import BeautifulSoup 28 | 29 | from CTkMessagebox import CTkMessagebox 30 | from PIL import Image 31 | 32 | # Use CTkToolTip and CTkListbox from my repo originally by Akascape (https://github.com/Akascape) 33 | from .CTkListbox.ctk_listbox import CTkListbox 34 | from .CTkToolTip.ctk_tooltip import CTkToolTip 35 | 36 | # winpty, sorry to my linux friends, blame Steamcmd for their awful output buffering 37 | from src.winpty_patch import PtyProcess, PatchedPtyProcess 38 | 39 | if getattr(sys, 'frozen', False): 40 | # If the application is run as a bundle, the PyInstaller bootloader 41 | # extends the sys module by a flag frozen=True and sets the app 42 | # path into variable _MEIPASS'. 43 | APPLICATION_PATH = os.path.dirname(sys.executable) 44 | else: 45 | APPLICATION_PATH = os.path.dirname(os.path.abspath(__file__)) 46 | 47 | # default enc key, change this to whatever you want in your ENV 48 | DEFAULT_ENV_KEY = 'iDd40QsvCwsntXuLniIbNd6cAJEcALd85QTEgEhIc1c=' 49 | BOIIIWD_ENC_KEY = base64.urlsafe_b64decode(os.getenv('BOIIIWD_ENC_KEY', DEFAULT_ENV_KEY)) 50 | 51 | # Constants 52 | CONFIG_FILE_PATH = "config.ini" 53 | GITHUB_REPO = "faroukbmiled/BOIIIWD" 54 | ITEM_INFO_API = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/" 55 | LATEST_RELEASE_URL = "https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip" 56 | LIBRARY_FILE = "boiiiwd_library.json" 57 | RESOURCES_DIR = os.path.join(os.path.dirname(__file__), '..', 'resources') 58 | UPDATER_FOLDER = "update" 59 | REGISTRY_KEY_PATH = r"Software\BOIIIWD" 60 | STEAMCMD_WARNING_COUNTER = 20 # how many times steamcmd fails before showing a non breaking warning 61 | SECONDS_UNTIL_FAIL_COUNTS = 15 # Minimum steamcmd runtime in seconds before a failed attempt counts as a fail (+1 to fail counter) 62 | FAIL_THRESHOLD_FALLBACK = 25 63 | DOWN_CAP = 15000000 # 15MB, workaround for psutil net_io_counters 64 | VERSION = "v0.3.7.3" 65 | -------------------------------------------------------------------------------- /boiiiwd_package/src/settings_tab.py: -------------------------------------------------------------------------------- 1 | from xml.sax.xmlreader import InputSource 2 | from src import library_tab 3 | from src.update_window import check_for_updates_func 4 | from src.imports import * 5 | from src.helpers import * 6 | 7 | import src.shared_vars as main_app 8 | 9 | 10 | class SettingsTab(ctk.CTkFrame): 11 | def __init__(self, master=None): 12 | super().__init__(master) 13 | # settings default bools 14 | self.skip_already_installed = True 15 | self.stopped = False 16 | self.console = False 17 | self.clean_on_finish = True 18 | self.continuous = True 19 | self.estimated_progress = True 20 | self.steam_fail_counter_toggle = True 21 | self.steam_fail_counter = 0 22 | self.steam_fail_number = 10 23 | self.steamcmd_reset = False 24 | self.show_fails = True 25 | self.check_items_on_launch = False 26 | 27 | # Left and right frames, use fg_color="transparent" 28 | self.grid_rowconfigure(0, weight=1) 29 | self.grid_columnconfigure(1, weight=1) 30 | self.grid_columnconfigure(0, weight=1) 31 | left_frame = ctk.CTkFrame(self) 32 | left_frame.grid(row=0, column=0, columnspan=2, padx=(20, 20), pady=(20, 0), sticky="nsew") 33 | left_frame.grid_columnconfigure(1, weight=1) 34 | right_frame = ctk.CTkFrame(self) 35 | right_frame.grid(row=0, column=2, padx=(0, 20), pady=(20, 0), sticky="nsew") 36 | right_frame.grid_columnconfigure(1, weight=1) 37 | self.update_idletasks() 38 | 39 | # Save button 40 | self.save_button = ctk.CTkButton(self, text="Save", command=self.save_settings, state='disabled') 41 | self.save_button.grid(row=3, column=1, padx=40, pady=(20, 20), sticky="ne") 42 | 43 | # Check for updates checkbox 44 | self.check_updates_var = ctk.BooleanVar() 45 | self.check_updates_var.trace_add("write", self.enable_save_button) 46 | self.check_updates_checkbox = ctk.CTkSwitch(left_frame, text="Check for updates on launch", variable=self.check_updates_var) 47 | self.check_updates_checkbox.grid(row=0, column=1, padx=20 , pady=(20, 0), sticky="nw") 48 | self.check_updates_var.set(self.load_settings("checkforupdates")) 49 | 50 | # Show console checkbox 51 | self.console_var = ctk.BooleanVar() 52 | self.console_var.trace_add("write", self.enable_save_button) 53 | self.checkbox_show_console = ctk.CTkSwitch(left_frame, text="Display console", variable=self.console_var, command=self.toggle_console_window) 54 | self.checkbox_show_console.grid(row=1, column=1, padx=20, pady=(20, 0), sticky="nw") 55 | self.checkbox_show_console_tooltip = CTkToolTip(self.checkbox_show_console, message="Toggle a console window\nPlease don't close the Console If you want to stop press the Stop button") 56 | self.console_var.set(self.load_settings("console")) 57 | 58 | # Show continuous checkbox 59 | self.continuous_var = ctk.BooleanVar() 60 | self.continuous_var.trace_add("write", self.enable_save_button) 61 | self.checkbox_continuous = ctk.CTkSwitch(left_frame, text="Continuous download", variable=self.continuous_var) 62 | self.checkbox_continuous.grid(row=2, column=1, padx=20, pady=(20, 0), sticky="nw") 63 | 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)") 64 | self.continuous_var.set(self.load_settings("continuous_download")) 65 | 66 | # clean on finish checkbox 67 | self.clean_checkbox_var = ctk.BooleanVar() 68 | self.clean_checkbox_var.trace_add("write", self.enable_save_button) 69 | self.clean_checkbox = ctk.CTkSwitch(left_frame, text="Clean on finish", variable=self.clean_checkbox_var) 70 | self.clean_checkbox.grid(row=3, column=1, padx=20, pady=(20, 0), sticky="nw") 71 | 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") 72 | self.clean_checkbox_var.set(self.load_settings("clean_on_finish", "on")) 73 | 74 | # Show estimated_progress checkbox 75 | self.estimated_progress_var = ctk.BooleanVar() 76 | self.estimated_progress_var.trace_add("write", self.enable_save_button) 77 | self.estimated_progress_cb = ctk.CTkSwitch(left_frame, text="Estimated progress bar", variable=self.estimated_progress_var) 78 | self.estimated_progress_cb.grid(row=4, column=1, padx=20, pady=(20, 0), sticky="nw") 79 | 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\ 80 | \nThis is not accurate ,it's better than with it off which is calculating the downloaded folder size which steamcmd dumps the full size mostly") 81 | self.estimated_progress_var.set(self.load_settings("estimated_progress", "on")) 82 | 83 | # Show fails checkbox 84 | self.show_fails_var = ctk.BooleanVar() 85 | self.show_fails_var.trace_add("write", self.enable_save_button) 86 | self.show_fails_cb = ctk.CTkSwitch(left_frame, text="Show fails", variable=self.show_fails_var) 87 | self.show_fails_cb.grid(row=5, column=1, padx=20, pady=(20, 0), sticky="nw") 88 | 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") 89 | self.estimated_progress_var.set(self.load_settings("show_fails", "on")) 90 | 91 | # Show skip_already_installed maps checkbox 92 | self.skip_already_installed_var = ctk.BooleanVar() 93 | self.skip_already_installed_var.trace_add("write", self.enable_save_button) 94 | self.skip_already_installed_ch = ctk.CTkSwitch(left_frame, text="Skip already installed maps", variable=self.skip_already_installed_var) 95 | self.skip_already_installed_ch.grid(row=6, column=1, padx=20, pady=(20, 0), sticky="nw") 96 | 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") 97 | self.skip_already_installed_var.set(self.load_settings("skip_already_installed", "on")) 98 | 99 | # check items for update on launch 100 | self.check_items_var = ctk.BooleanVar() 101 | self.check_items_var.trace_add("write", self.enable_save_button) 102 | self.check_items_ch = ctk.CTkSwitch(left_frame, text="Check library items on launch", variable=self.check_items_var) 103 | self.check_items_ch.grid(row=7, column=1, padx=20, pady=(20, 0), sticky="nw") 104 | self.check_items_tooltip = CTkToolTip(self.check_items_ch, message="This will show a window on launch of items that have pending updates -> you can open it manually from library tab") 105 | self.check_items_var.set(self.load_settings("check_items", "off")) 106 | 107 | 108 | # TODO: get windows size to padx for the following checkboxes 109 | 110 | # update invalid 111 | self.invalid_items_var = ctk.BooleanVar() 112 | self.invalid_items_var.trace_add("write", self.enable_save_button) 113 | self.invalid_items_ch = ctk.CTkSwitch(left_frame, text="Update invalid items", variable=self.invalid_items_var) 114 | self.invalid_items_ch.grid(row=0, column=1, padx=(300,0), pady=(20, 0), sticky="nw") 115 | self.invalid_items_tooltip = CTkToolTip(self.invalid_items_ch, message="Allow updating invalid items from the details tab") 116 | self.invalid_items_var.set(self.load_settings("update_invalid", "off")) 117 | 118 | # skip invalid 119 | self.skip_items_var = ctk.BooleanVar() 120 | self.skip_items_var.trace_add("write", self.enable_save_button) 121 | self.skip_items_ch = ctk.CTkSwitch(left_frame, text="Skip invalid items", variable=self.skip_items_var) 122 | self.skip_items_ch.grid(row=1, column=1, padx=(300,0), pady=(20, 0), sticky="nw") 123 | self.skip_items_tooltip = CTkToolTip(self.skip_items_ch, message="Skip invalid items") 124 | self.skip_items_var.set(self.load_settings("skip_invalid", "off")) 125 | 126 | # USING CREDENTIALS 127 | self.use_steam_creds = ctk.BooleanVar() 128 | self.use_steam_creds.trace_add("write", self.enable_save_button) 129 | self.use_steam_creds_sw = ctk.CTkSwitch(left_frame, text="Use Steam Credentials", variable=self.use_steam_creds, command=self.use_steam_creds_inputs) 130 | self.use_steam_creds_sw.grid(row=2, column=1, padx=(300,0), pady=(20, 0), sticky="nw") 131 | self.use_steam_creds_tooltip = CTkToolTip(self.use_steam_creds_sw, message="Use your steam login to download (better dowload stability)") 132 | self.use_steam_creds.set(self.load_settings("use_steam_creds", "off")) 133 | 134 | # text input fields 135 | self.label_destination_folder = ctk.CTkLabel(left_frame, text='Enter Game folder:') 136 | self.label_destination_folder.grid(row=8, column=1, padx=20, pady=(20, 0), columnspan=1, sticky="ws") 137 | 138 | self.entry_var1 = ctk.StringVar(value="") 139 | self.edit_destination_folder = ctk.CTkEntry(left_frame, placeholder_text="game installation folder", textvariable=self.entry_var1) 140 | self.edit_destination_folder.grid(row=9, column=1, padx=20, pady=(0, 10), columnspan=1, sticky="ewn") 141 | self.entry_var1.trace_add("write", self.enable_save_button) 142 | 143 | self.button_BOIII_browse = ctk.CTkButton(left_frame, text="Select", command=self.open_BOIII_browser) 144 | self.button_BOIII_browse.grid(row=9, column=2, padx=(0, 20), pady=(0, 10), sticky="ewn") 145 | 146 | self.label_steamcmd_path = ctk.CTkLabel(left_frame, text="Enter SteamCMD path:") 147 | self.label_steamcmd_path.grid(row=10, column=1, padx=20, pady=(0, 0), columnspan=1, sticky="wn") 148 | 149 | self.entry_var2 = ctk.StringVar(value="") 150 | self.edit_steamcmd_path = ctk.CTkEntry(left_frame, placeholder_text="Enter SteamCMD path", textvariable=self.entry_var2) 151 | self.edit_steamcmd_path.grid(row=11, column=1, padx=20, pady=(0, 10), columnspan=1, sticky="ewn") 152 | self.entry_var2.trace_add("write", self.enable_save_button) 153 | 154 | self.button_steamcmd_browse = ctk.CTkButton(left_frame, text="Select", command=self.open_steamcmd_path_browser) 155 | self.button_steamcmd_browse.grid(row=11, column=2, padx=(0, 20), pady=(0, 10), sticky="ewn") 156 | 157 | self.label_launch_args = ctk.CTkLabel(left_frame, text='Launch Parameters:') 158 | self.label_launch_args.grid(row=12, column=1, padx=20, pady=(0, 0), columnspan=1, sticky="ws") 159 | 160 | self.edit_startup_exe = ctk.CTkEntry(left_frame, placeholder_text="exe") 161 | self.edit_startup_exe.grid(row=13, column=1, padx=(20,0), pady=(0, 20), columnspan=1, sticky="we") 162 | 163 | self.edit_launch_args = ctk.CTkEntry(left_frame, placeholder_text="launch arguments") 164 | self.edit_launch_args.grid(row=13, column=1, padx=(140,20), pady=(0, 20), columnspan=2, sticky="we") 165 | 166 | # Check for updates button n Launch game 167 | self.check_for_updates = ctk.CTkButton(right_frame, text="Check for updates", command=self.settings_check_for_updates) 168 | self.check_for_updates.grid(row=1, column=1, padx=20, pady=(20, 0), sticky="n") 169 | 170 | # self.launch_game = ctk.CTkButton(right_frame, text="Launch game", command=self.settings_launch_game) 171 | # self.launch_game.grid(row=2, column=1, padx=20, pady=(10, 0), sticky="n") 172 | 173 | self.reset_steamcmd = ctk.CTkButton(right_frame, text="Reset SteamCMD", command=self.settings_reset_steamcmd) 174 | self.reset_steamcmd.grid(row=3, column=1, padx=20, pady=(10, 0), sticky="n") 175 | self.reset_steamcmd_tooltip = CTkToolTip(self.reset_steamcmd, message="This will remove steamapps folder + all the maps that are potentioaly corrupted\nor not so use at ur own risk (could fix some issues as well)") 176 | 177 | self.workshop_to_gamedir = ctk.CTkButton(right_frame, text="Workshop Transfer", command=self.workshop_to_gamedir_toplevel) 178 | self.workshop_to_gamedir.grid(row=4, column=1, padx=20, pady=(10, 0), sticky="n") 179 | self.workshop_to_gamedir_tooltip = CTkToolTip(self.workshop_to_gamedir, message="Copy/Move maps and mods from Workshop to the game directory (opens up a window)") 180 | 181 | # appearance 182 | self.appearance_mode_label = ctk.CTkLabel(right_frame, text="Appearance:", anchor="w") 183 | self.appearance_mode_label.grid(row=5, column=1, padx=20, pady=(150, 0), sticky="nw") 184 | self.appearance_mode_optionemenu = ctk.CTkOptionMenu(right_frame, values=["Light", "Dark", "System"], 185 | command=master.change_appearance_mode_event) 186 | self.appearance_mode_optionemenu.grid(row=6, column=1, padx=20, pady=(0, 0), sticky="nw") 187 | self.scaling_label = ctk.CTkLabel(right_frame, text="UI Scaling:", anchor="w") 188 | self.scaling_label.grid(row=7, column=1, padx=20, pady=(0, 0), sticky="nw") 189 | self.scaling_optionemenu = ctk.CTkOptionMenu(right_frame, values=["60%", "70%", "80%", "90%", "100%", "110%"], 190 | command=master.change_scaling_event) 191 | self.scaling_optionemenu.grid(row=8, column=1, padx=20, pady=(0, 0), sticky="nw") 192 | 193 | # self.custom_theme = ctk.CTkButton(right_frame, text="Custom theme", command=self.boiiiwd_custom_theme) 194 | # self.custom_theme.grid(row=9, column=1, padx=20, pady=(20, 0), sticky="n") 195 | 196 | self.theme_options_label = ctk.CTkLabel(right_frame, text="Theme:", anchor="w") 197 | self.theme_options_label.grid(row=9, column=1, padx=20, pady=(0, 0), sticky="nw") 198 | self.theme_options = ctk.CTkOptionMenu(right_frame, values=["Default", "Blue", "Grey", "Obsidian", "Ghost","NeonBanana", "Custom"], 199 | command=self.theme_options_func) 200 | self.theme_options.grid(row=10, column=1, padx=20, pady=(0, 0), sticky="nw") 201 | self.theme_options.set(value=self.load_settings("theme", "Default")) 202 | 203 | # Reset steam on many fails 204 | self.reset_steamcmd_on_fail_var = ctk.IntVar() 205 | self.reset_steamcmd_on_fail_var.trace_add("write", self.enable_save_button) 206 | self.reset_steamcmd_on_fail_text = ctk.CTkLabel(right_frame, text=f"Download Attempts:", anchor="w") 207 | self.reset_steamcmd_on_fail_text.grid(row=11, column=1, padx=20, pady=(0, 0), sticky="nw") 208 | self.reset_steamcmd_on_fail = ctk.CTkOptionMenu(right_frame, values=["5", "10", "15", "20", "Custom", "Disable"], variable=self.reset_steamcmd_on_fail_var, command=self.reset_steamcmd_on_fail_func) 209 | self.reset_steamcmd_on_fail.grid(row=12, column=1, padx=20, pady=(0, 0), sticky="nw") 210 | self.reset_steamcmd_on_fail_tooltip = CTkToolTip(self.reset_steamcmd_on_fail, message="Number of failed download attempts before resetting SteamCMD") 211 | self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "10")) 212 | 213 | # item folder naming 214 | self.folder_options_label_var = ctk.IntVar() 215 | self.folder_options_label_var.trace_add("write", self.enable_save_button) 216 | self.folder_options_label = ctk.CTkLabel(right_frame, text="Items Folder Naming:", anchor="w") 217 | self.folder_options_label.grid(row=13, column=1, padx=(20,0), pady=(0, 0), sticky="nw") 218 | self.folder_options = ctk.CTkOptionMenu(right_frame, values=["PublisherID", "FolderName"], command=self.change_folder_naming, 219 | variable=self.folder_options_label_var) 220 | self.folder_options.grid(row=14, column=1, padx=20, pady=(0, 0), sticky="nw") 221 | self.folder_options.set(value=self.load_settings("folder_naming", "PublisherID")) 222 | 223 | #version 224 | self.version_info = ctk.CTkLabel(self, text=f"{VERSION}") 225 | self.version_info.grid(row=3, column=2, padx=20, pady=(20, 20), sticky="e") 226 | 227 | def open_BOIII_browser(self): 228 | selected_folder = ctk.filedialog.askdirectory(title="Select Game Folder") 229 | if selected_folder: 230 | self.edit_destination_folder.delete(0, "end") 231 | self.edit_destination_folder.insert(0, selected_folder) 232 | save_config("DestinationFolder" ,self.edit_destination_folder.get()) 233 | save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) 234 | 235 | def open_steamcmd_path_browser(self): 236 | selected_folder = ctk.filedialog.askdirectory(title="Select SteamCMD Folder") 237 | if selected_folder: 238 | self.edit_steamcmd_path.delete(0, "end") 239 | self.edit_steamcmd_path.insert(0, selected_folder) 240 | save_config("DestinationFolder" ,self.edit_destination_folder.get()) 241 | save_config("SteamCMDPath" ,self.edit_steamcmd_path.get()) 242 | 243 | def reset_steamcmd_on_fail_func(self, option: str): 244 | if option == "Custom": 245 | try: 246 | default_val = check_config("reset_on_fail", "10") 247 | def callback(): 248 | input_value = input_popup("Number of fails", "Enter a number of failed attempts before resetting SteamCMD (higher number is recommended)") 249 | if input_value and input_value.strip() and input_value.isdigit(): 250 | save_config("reset_on_fail", str(input_value)) 251 | self.reset_steamcmd_on_fail.set(input_value) 252 | elif input_value is not None: 253 | show_message("Invalid input", "Please enter a valid number") 254 | self.reset_steamcmd_on_fail.set(default_val) 255 | else: 256 | self.reset_steamcmd_on_fail.set(default_val) 257 | self.after(0, callback) 258 | except Exception as e: 259 | print(f"[{get_current_datetime()}] [Logs] Error in reset_steamcmd_on_fail_func: {e}") 260 | else: 261 | return 262 | 263 | def theme_options_func(self, option: str): 264 | theme_mapping = { 265 | "Default": "boiiiwd_theme.json", 266 | "Blue": "boiiiwd_blue.json", 267 | "Grey": "boiiiwd_grey.json", 268 | "Ghost": "boiiiwd_ghost.json", 269 | "Obsidian": "boiiiwd_obsidian.json", 270 | "NeonBanana": "boiiiwd_neonbanana.json", 271 | "Custom": "boiiiwd_theme.json", 272 | } 273 | 274 | theme_file = theme_mapping.get(option) 275 | 276 | if option == "Custom": 277 | self.boiiiwd_custom_theme() 278 | save_config("theme", "boiiiwd_theme.json") 279 | return 280 | 281 | if theme_file: 282 | self.boiiiwd_custom_theme(disable_only=True) 283 | save_config("theme", theme_file) 284 | else: 285 | return 286 | 287 | if option != "Custom": 288 | if show_message("Restart to take effect!", f"{option} theme has been set, please restart to take effect", icon="info", _return=True, option_1="Ok", option_2="Restart"): 289 | try: 290 | p = psutil.Process(os.getpid()) 291 | for handler in p.open_files() + p.connections(): 292 | os.close(handler.fd) 293 | except Exception: 294 | pass 295 | python = sys.executable 296 | os.execl(python, python, *sys.argv) 297 | 298 | def enable_save_button(self, *args): 299 | try: self.save_button.configure(state='normal') 300 | except: pass 301 | 302 | # TODO: cant be bothered to refactor this, at a later date maybe 303 | def save_settings(self): 304 | self.save_button.configure(state='disabled') 305 | 306 | save_config("GameExecutable", str(self.edit_startup_exe.get()) if not isNullOrWhiteSpace(self.edit_startup_exe.get()) else "BlackOps3") 307 | save_config("LaunchParameters", str(self.edit_launch_args.get()) if not isNullOrWhiteSpace(self.edit_launch_args.get()) else " ") 308 | 309 | if self.folder_options.get() == "PublisherID": 310 | save_config("folder_naming", "0") 311 | else: 312 | save_config("folder_naming", "1") 313 | 314 | if self.check_items_var.get(): 315 | save_config("check_items", "on") 316 | else: 317 | save_config("check_items", "off") 318 | 319 | if self.invalid_items_var.get(): 320 | save_config("update_invalid", "on") 321 | else: 322 | save_config("update_invalid", "off") 323 | 324 | if self.skip_items_var.get(): 325 | save_config("skip_invalid", "on") 326 | else: 327 | save_config("skip_invalid", "off") 328 | 329 | if self.check_updates_checkbox.get(): 330 | save_config("checkforupdates", "on") 331 | else: 332 | save_config("checkforupdates", "off") 333 | 334 | if self.checkbox_show_console.get(): 335 | save_config("console", "on") 336 | self.console = True 337 | else: 338 | save_config("console", "off") 339 | self.console = False 340 | 341 | if self.skip_already_installed_ch.get(): 342 | save_config("skip_already_installed", "on") 343 | self.skip_already_installed = True 344 | else: 345 | save_config("skip_already_installed", "off") 346 | self.skip_already_installed = False 347 | 348 | if self.clean_checkbox.get(): 349 | save_config("clean_on_finish", "on") 350 | self.clean_on_finish = True 351 | else: 352 | save_config("clean_on_finish", "off") 353 | self.clean_on_finish = False 354 | 355 | if self.checkbox_continuous.get(): 356 | save_config("continuous_download", "on") 357 | self.continuous = True 358 | else: 359 | save_config("continuous_download", "off") 360 | self.continuous = False 361 | 362 | if self.estimated_progress_cb.get(): 363 | save_config("estimated_progress", "on") 364 | self.estimated_progress = True 365 | else: 366 | save_config("estimated_progress", "off") 367 | self.estimated_progress = False 368 | 369 | if self.show_fails_cb.get(): 370 | save_config("show_fails", "on") 371 | self.show_fails = True 372 | else: 373 | save_config("show_fails", "off") 374 | self.show_fails = False 375 | 376 | if self.reset_steamcmd_on_fail.get(): 377 | value = self.reset_steamcmd_on_fail.get() 378 | if value == "Disable": 379 | self.steam_fail_counter_toggle = False 380 | elif value == "Custom": 381 | self.steam_fail_counter_toggle = True 382 | self.steam_fail_number = int(check_config("reset_on_fail", "10")) 383 | self.reset_steamcmd_on_fail.set(check_config("reset_on_fail", "10")) 384 | else: 385 | self.steam_fail_counter_toggle = True 386 | self.steam_fail_number = int(value) 387 | save_config("reset_on_fail", value) 388 | 389 | # TODO: cant be bothered to refactor this, at a later date maybe 390 | def load_settings(self, setting, fallback=None): 391 | if setting == "folder_naming": 392 | if check_config(setting, fallback) == "1": 393 | return "FolderName" 394 | else: 395 | return "PublisherID" 396 | 397 | if setting == "console": 398 | if check_config(setting, fallback) == "on": 399 | show_console() 400 | self.console = True 401 | return 1 402 | else: 403 | self.console = False 404 | return 0 405 | 406 | if setting == "continuous_download": 407 | if check_config(setting, "on") == "on": 408 | self.continuous = True 409 | return 1 410 | else: 411 | self.continuous = False 412 | return 0 413 | 414 | if setting == "clean_on_finish": 415 | if check_config(setting, fallback) == "on": 416 | self.clean_on_finish = True 417 | return 1 418 | else: 419 | self.clean_on_finish = False 420 | return 0 421 | if setting == "estimated_progress": 422 | if check_config(setting, fallback) == "on": 423 | self.estimated_progress = True 424 | return 1 425 | else: 426 | self.estimated_progress = False 427 | return 0 428 | 429 | if setting == "reset_on_fail": 430 | option = check_config(setting, fallback) 431 | if option == "Disable" or option == "Custom": 432 | self.steam_fail_counter_toggle = False 433 | return "Disable" 434 | else: 435 | try: 436 | self.steam_fail_number = int(option) 437 | self.steam_fail_counter_toggle = True 438 | return option 439 | except: 440 | self.steam_fail_counter_toggle = True 441 | self.steam_fail_number = 10 442 | return "10" 443 | 444 | if setting == "show_fails": 445 | if check_config(setting, fallback) == "on": 446 | self.show_fails = True 447 | return 1 448 | else: 449 | self.show_fails = False 450 | return 0 451 | 452 | if setting == "skip_already_installed": 453 | if check_config(setting, fallback) == "on": 454 | self.skip_already_installed = True 455 | return 1 456 | else: 457 | self.skip_already_installed = False 458 | return 0 459 | 460 | if setting == "theme": 461 | theme_config = check_config("theme", "boiiiwd_theme.json") 462 | 463 | if os.path.exists(os.path.join(APPLICATION_PATH, theme_config)): 464 | return "Custom" 465 | 466 | if theme_config == "boiiiwd_theme.json": 467 | return "Default" 468 | 469 | match = re.match(r'boiiiwd_(\w+)\.json', theme_config) 470 | if match: 471 | theme_name = match.group(1).capitalize() 472 | return theme_name 473 | else: 474 | return 1 if check_config(setting, fallback) == "on" else 0 475 | 476 | def boiiiwd_custom_theme(self, disable_only=None): 477 | file_to_rename = os.path.join(APPLICATION_PATH, "boiiiwd_theme.json") 478 | if os.path.exists(file_to_rename): 479 | timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") 480 | name = f"boiiiwd_theme_{timestamp}.json" 481 | os.rename(file_to_rename, os.path.join(APPLICATION_PATH, name)) 482 | 483 | if not disable_only: 484 | show_message("Preset file renamed", "Custom preset disabled, file has been renmaed\n* Restart the app to take effect", icon="info") 485 | else: 486 | if disable_only: 487 | return 488 | try: 489 | shutil.copy(os.path.join(RESOURCES_DIR, check_config("theme", "boiiiwd_theme.json")), os.path.join(APPLICATION_PATH, "boiiiwd_theme.json")) 490 | except: 491 | shutil.copy(os.path.join(RESOURCES_DIR, "boiiiwd_theme.json"), os.path.join(APPLICATION_PATH, "boiiiwd_theme.json")) 492 | show_message("Preset file created", "You can now edit boiiiwd_theme.json in the current directory to your liking\n* Edits will apply next time you open boiiiwd\n* Program will always take boiiiwd_theme.json as the first theme option if found\n* Click on this button again to disable your custom theme or just rename boiiiwd_theme.json", icon="info") 493 | 494 | def settings_check_for_updates(self): 495 | check_for_updates_func(self, ignore_up_todate=False) 496 | 497 | # make this rename to {id}_duplicate as a fallback 498 | def rename_all_folders(self, option): 499 | gameFolder = self.edit_destination_folder.get() 500 | mods_folder = os.path.join(gameFolder, "mods") 501 | maps_folder = os.path.join(gameFolder, "usermaps") 502 | 503 | folders_to_process = [] 504 | 505 | if os.path.exists(mods_folder): 506 | folders_to_process.append(mods_folder) 507 | 508 | if os.path.exists(maps_folder): 509 | folders_to_process.append(maps_folder) 510 | 511 | if not os.path.exists(maps_folder) and not os.path.exists(mods_folder): 512 | show_message("Warning -> Check game path", 513 | f"You don't have any items yet, from now on item's folders will be named as their {option}") 514 | return 0 515 | 516 | processed_names = set() 517 | 518 | files = Path(folders_to_process[0]).glob("*/zone/workshop.json") 519 | items = dict() 520 | ignored_folders = [] 521 | 522 | for idx, file in enumerate(files): 523 | curr_folder_name = os.path.relpath(file, folders_to_process[0]).split("\\", 1)[0] 524 | 525 | with open(file, 'r', errors="ignore") as json_file: 526 | data = json.load(json_file) 527 | _item = { 528 | 'PublisherID': data.get('PublisherID'), 529 | 'Name': data.get(option), 530 | 'current_folder': curr_folder_name 531 | } 532 | if _item.get('PublisherID')!="" and int(_item.get('PublisherID'))>0: 533 | items[idx] = _item 534 | else: 535 | ignored_folders.append(curr_folder_name) 536 | 537 | IDs = [x['PublisherID'] for x in items.values()] 538 | Names = [x['Name'] for x in items.values()] 539 | currFolder = [x['current_folder'] for x in items.values()] 540 | 541 | def indices(lst, item): 542 | return [i for i, x in enumerate(lst) if item in x] 543 | 544 | def find_duplicate_items_in_list(list, items): 545 | return dict((x, indices(list, x)) for x in [y for y in items if items.count(y) > 1]) 546 | 547 | def prep_rename(changelist, orig, new): 548 | return changelist.append((os.path.join(folders_to_process[0], orig), os.path.join(folders_to_process[0], new))) 549 | 550 | duplicates = find_duplicate_items_in_list(Names, Names) 551 | duplicates_IDs = find_duplicate_items_in_list(IDs, IDs) 552 | 553 | duplicate_idx = concatenate_sublists(duplicates.values()) 554 | 555 | changelist = [] 556 | 557 | for i in range(len(IDs)): 558 | if i not in duplicate_idx: 559 | prep_rename(changelist, currFolder[i], Names[i]) 560 | 561 | for v in duplicates.values(): 562 | if len(v) == 2: 563 | if IDs[v[0]] == IDs[v[1]]: 564 | prep_rename(changelist, currFolder[v[0]], Names[v[0]]) 565 | prep_rename( 566 | changelist, currFolder[v[1]], Names[v[1]]+"_duplicate") 567 | else: 568 | prep_rename( 569 | changelist, currFolder[v[0]], Names[v[0]]+f"_{IDs[v[0]]}") 570 | prep_rename( 571 | changelist, currFolder[v[1]], Names[v[1]]+f"_{IDs[v[1]]}") 572 | 573 | if len(v) > 2: 574 | for j, i in enumerate(v): 575 | if i in (duplicates_IDs.get(f'{IDs[i]}') if duplicates_IDs.get(f'{IDs[i]}') is not None else []): 576 | if i == v[0]: 577 | if Names[i].startswith(IDs[i]): 578 | prep_rename( 579 | changelist, currFolder[i], Names[i]) 580 | else: 581 | prep_rename( 582 | changelist, currFolder[i], Names[i]+f"_{IDs[i]}") 583 | else: 584 | if Names[i].startswith(IDs[i]): 585 | newname = Names[i]+f"_duplicate" 586 | else: 587 | newname = Names[i]+f"_{IDs[i]}" 588 | 589 | if j > 0: 590 | newname += '_' + str(j) 591 | 592 | prep_rename(changelist, currFolder[i], newname) 593 | else: 594 | prep_rename( 595 | changelist, currFolder[i], Names[i]+f"_{IDs[i]}") 596 | 597 | for n in changelist: 598 | safe_name = nextnonexistentdir(*tuple(reversed(os.path.split(n[1])))) 599 | if safe_name[0] in ignored_folders and safe_name[1] > 0: 600 | os.rename(n[0], os.path.join(n[0], '{}_{}'.format(*safe_name))) 601 | else: 602 | os.rename(n[0], n[1]) 603 | 604 | return 1 605 | 606 | def change_folder_naming(self, option): 607 | main_app.app.title("BOIII Workshop Downloader - Settings ➜ Loading... ⏳") 608 | try: 609 | if os.path.exists(self.edit_destination_folder.get()): 610 | lib = main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) 611 | if not "No items" in lib: 612 | if show_message("Renaming", "Would you like to rename all your exisiting item folders now?", _return=True): 613 | main_app.app.title("BOIII Workshop Downloader - Settings ➜ Renaming... ⏳") 614 | try : ren_return = self.rename_all_folders(option) 615 | except Exception as er: show_message("Error!", f"Error occured when renaming\n{er}"); return 616 | if ren_return == 0: 617 | return 0 618 | else: 619 | show_message("Done!", "All folders have been renamed", icon="info") 620 | main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) 621 | else: 622 | show_message("Heads up!", "Only newly downloaded items will be affected", icon="info") 623 | else: 624 | show_message("Warning -> Check game path", f"You don't have any items yet ,from now on item's folders will be named as their {option}") 625 | else: 626 | show_message("Warning -> Check game path", f"You don't have any items yet ,from now on item's folders will be named as their {option}") 627 | except Exception as e: 628 | show_message("Error", f"Error occured \n{e}") 629 | finally: 630 | main_app.app.title("BOIII Workshop Downloader - Settings") 631 | self.save_settings() 632 | 633 | def load_on_switch_screen(self): 634 | self.check_updates_var.set(self.load_settings("checkforupdates")) 635 | self.console_var.set(self.load_settings("console")) 636 | self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "10")) 637 | self.estimated_progress_var.set(self.load_settings("estimated_progress", "on")) 638 | self.clean_checkbox_var.set(self.load_settings("clean_on_finish", "on")) 639 | self.continuous_var.set(self.load_settings("continuous_download")) 640 | self.show_fails_var.set(self.load_settings("show_fails", "on")) 641 | self.skip_already_installed_var.set(self.load_settings("skip_already_installed", "on")) 642 | 643 | # keep last cuz of trace_add() 644 | self.save_button.configure(state='disabled') 645 | 646 | def settings_launch_game(self): 647 | launch_game_func(check_config("destinationfolder"), self.edit_startup_exe.get(), self.edit_launch_args.get()) 648 | 649 | def settings_reset_steamcmd(self): 650 | reset_steamcmd() 651 | 652 | def workshop_to_gamedir_toplevel(self): 653 | try: 654 | # to make sure json file is up to date 655 | main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) 656 | top = ctk.CTkToplevel(self) 657 | if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")): 658 | top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico"))) 659 | top.title("Workshop Transfer") 660 | _, _, x, y = get_window_size_from_registry() 661 | top.geometry(f"+{x}+{y}") 662 | # top.attributes('-topmost', 'true') 663 | top.resizable(False, False) 664 | # Create input boxes 665 | center_frame = ctk.CTkFrame(top) 666 | 667 | # Create input boxes 668 | steam_folder_label = ctk.CTkLabel(center_frame, text="Steam Folder:") 669 | steam_folder_entry = ctk.CTkEntry(center_frame, width=225) 670 | button_steam_browse = ctk.CTkButton(center_frame, text="Select", width=10) 671 | game_folder_label = ctk.CTkLabel(center_frame, text="Game Folder:") 672 | game_folder_entry = ctk.CTkEntry(center_frame, width=225) 673 | button_BOIII_browse = ctk.CTkButton(center_frame, text="Select", width=10) 674 | 675 | # Create option to choose between cut or copy 676 | operation_label = ctk.CTkLabel(center_frame, text="Choose operation:") 677 | copy_var = ctk.BooleanVar() 678 | cut_var = ctk.BooleanVar() 679 | copy_check = ctk.CTkCheckBox(center_frame, text="Copy", variable=copy_var) 680 | cut_check = ctk.CTkCheckBox(center_frame, text="Cut", variable=cut_var) 681 | 682 | # Create progress bar 683 | progress_bar = ctk.CTkProgressBar(center_frame, mode="determinate", height=20, corner_radius=7) 684 | progress_text = ctk.CTkLabel(progress_bar, text="0%", font=("Helvetica", 12), fg_color="transparent", text_color="white", height=0, width=0, corner_radius=0) 685 | copy_button = ctk.CTkButton(center_frame, text="Start (Copy)") 686 | 687 | # funcs 688 | # had to use this shit again cuz of threading issues with widgets 689 | def copy_with_progress(src, dst): 690 | try: 691 | total_files = sum([len(files) for root, dirs, files in os.walk(src)]) 692 | progress = 0 693 | 694 | def copy_progress(src, dst): 695 | nonlocal progress 696 | shutil.copy2(src, dst) 697 | progress += 1 698 | top.after(0, progress_text.configure(text=f"Copying files: {progress}/{total_files}")) 699 | value = (progress / total_files) * 100 700 | valuep = value / 100 701 | progress_bar.set(valuep) 702 | 703 | try: 704 | shutil.copytree(src, dst, dirs_exist_ok=True, copy_function=copy_progress) 705 | except Exception as E: 706 | show_message("Error", f"Error copying files: {E}", icon="cancel") 707 | finally: 708 | top.after(0, progress_text.configure(text="0%")) 709 | top.after(0, progress_bar.set(0.0)) 710 | 711 | def check_status(var, op_var): 712 | if var.get(): 713 | op_var.set(False) 714 | if cut_var.get(): 715 | copy_button.configure(text=f"Start (Cut)") 716 | if copy_var.get(): 717 | copy_button.configure(text=f"Start (Copy)") 718 | 719 | def open_game_browser(): 720 | selected_folder = ctk.filedialog.askdirectory(title="Select Game Folder") 721 | if selected_folder: 722 | game_folder_entry.delete(0, "end") 723 | game_folder_entry.insert(0, selected_folder) 724 | 725 | def open_steam_browser(): 726 | selected_folder = ctk.filedialog.askdirectory(title="Select Steam Folder (ex: C:/Program Files (x86)/Steam)") 727 | if selected_folder: 728 | steam_folder_entry.delete(0, "end") 729 | steam_folder_entry.insert(0, selected_folder) 730 | save_config("steam_folder" ,steam_folder_entry.get()) 731 | 732 | def start_copy_operation(): 733 | def start_thread(): 734 | try: 735 | if not cut_var.get() and not copy_var.get(): 736 | show_message("Choose operation!", "Please choose an operation, Copy or Cut files from Steam!") 737 | return 738 | 739 | copy_button.configure(state="disabled") 740 | steam_folder = steam_folder_entry.get() 741 | ws_folder = os.path.join(steam_folder, "steamapps/workshop/content/311210") 742 | game_folder = game_folder_entry.get() 743 | 744 | if not os.path.exists(steam_folder) and not os.path.exists(ws_folder): 745 | show_message("Not found", "Either you have no items downloaded from Steam or wrong path, please recheck path (ex: C:/Program Files (x86)/Steam)") 746 | return 747 | 748 | if not os.path.exists(game_folder): 749 | show_message("Not found", "game folder not found, please recheck path") 750 | return 751 | 752 | top.after(0, progress_text.configure(text="Loading...")) 753 | 754 | map_folder = os.path.join(ws_folder) 755 | 756 | subfolders = [f for f in os.listdir(map_folder) if os.path.isdir(os.path.join(map_folder, f))] 757 | total_folders = len(subfolders) 758 | 759 | if not subfolders: 760 | show_message("No items found", f"No items found in \n{map_folder}") 761 | return 762 | 763 | for i, dir_name in enumerate(subfolders, start=1): 764 | json_file_path = os.path.join(map_folder, dir_name, "workshop.json") 765 | copy_button.configure(text=f"Working on -> {i}/{total_folders}") 766 | 767 | if os.path.exists(json_file_path): 768 | workshop_id = extract_json_data(json_file_path, "PublisherID") 769 | mod_type = extract_json_data(json_file_path, "Type") 770 | items_file = os.path.join(APPLICATION_PATH, LIBRARY_FILE) 771 | item_exists,_ = main_app.app.library_tab.item_exists_in_file(items_file, workshop_id) 772 | 773 | if item_exists: 774 | get_folder_name = main_app.app.library_tab.get_item_by_id(items_file, workshop_id, return_option="folder_name") 775 | if get_folder_name: 776 | folder_name = get_folder_name 777 | else: 778 | try: 779 | folder_name = extract_json_data(json_file_path, main_app.app.settings_tab.folder_options.get()) 780 | except: 781 | folder_name = extract_json_data(json_file_path, "publisherID") 782 | else: 783 | try: 784 | folder_name = extract_json_data(json_file_path, main_app.app.settings_tab.folder_options.get()) 785 | except: 786 | folder_name = extract_json_data(json_file_path, "publisherID") 787 | 788 | if mod_type == "mod": 789 | path_folder = os.path.join(game_folder, "mods") 790 | folder_name_path = os.path.join(path_folder, folder_name, "zone") 791 | elif mod_type == "map": 792 | path_folder = os.path.join(game_folder, "usermaps") 793 | folder_name_path = os.path.join(path_folder, folder_name, "zone") 794 | else: 795 | show_message("Error", "Invalid workshop type in workshop.json, are you sure this is a map or a mod?.", icon="cancel") 796 | continue 797 | 798 | if not item_exists: 799 | while os.path.exists(os.path.join(path_folder, folder_name)): 800 | folder_name += f"_{workshop_id}" 801 | folder_name_path = os.path.join(path_folder, folder_name, "zone") 802 | 803 | os.makedirs(folder_name_path, exist_ok=True) 804 | 805 | try: 806 | copy_with_progress(os.path.join(map_folder, dir_name), folder_name_path) 807 | except Exception as E: 808 | show_message("Error", f"Error copying files: {E}", icon="cancel") 809 | continue 810 | 811 | if cut_var.get(): 812 | remove_tree(os.path.join(map_folder, dir_name)) 813 | 814 | main_app.app.library_tab.update_item(self.edit_destination_folder.get(), workshop_id, mod_type, folder_name) 815 | else: 816 | # if its last folder to check 817 | if i == total_folders: 818 | show_message("Error", f"workshop.json not found in {dir_name}", icon="cancel") 819 | main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) 820 | return 821 | continue 822 | 823 | if subfolders: 824 | main_app.app.library_tab.load_items(self.edit_destination_folder.get(), dont_add=True) 825 | main_app.app.show_complete_message(message=f"All items were moved\nYou can run the game now!\nPS: You have to restart the game\n(pressing launch will launch/restarts)") 826 | 827 | finally: 828 | if cut_var.get(): 829 | copy_button.configure(text=f"Start (Cut)") 830 | if copy_var.get(): 831 | copy_button.configure(text=f"Start (Copy)") 832 | copy_button.configure(state="normal") 833 | top.after(0, progress_bar.set(0)) 834 | top.after(0, progress_text.configure(text="0%")) 835 | 836 | # prevents app hanging 837 | threading.Thread(target=start_thread).start() 838 | 839 | # config 840 | center_frame.grid(row=0, column=0, padx=20, pady=20) 841 | button_steam_browse.grid(row=1, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes") 842 | steam_folder_label.grid(row=0, column=0, padx=(20, 20), pady=(10, 0), sticky='w') 843 | steam_folder_entry.grid(row=1, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes') 844 | game_folder_label.grid(row=2, column=0, padx=(20, 20), pady=(10, 0), sticky='w') 845 | game_folder_entry.grid(row=3, column=0, columnspan=2, padx=(0, 20), pady=(10, 10), sticky='nes') 846 | button_BOIII_browse.grid(row=3, column=2, padx=(0, 20), pady=(10, 10), sticky="wnes") 847 | operation_label.grid(row=4, column=0, padx=(20, 20), pady=(10, 10), sticky='wnes') 848 | copy_check.grid(row=4, column=1, padx=(0, 10), pady=(10, 10), sticky='wnes') 849 | cut_check.grid(row=4, column=2, padx=(0, 10), pady=(10, 10), sticky='nes') 850 | progress_bar.grid(row=5, column=0, columnspan=3, padx=(20, 20), pady=(10, 10), sticky='wnes') 851 | progress_text.place(relx=0.5, rely=0.5, anchor="center") 852 | copy_button.grid(row=6, column=0, columnspan=3,padx=(20, 20), pady=(10, 10), sticky='wnes') 853 | progress_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "progress_bar_fill_color") 854 | progress_bar.configure(progress_color=progress_color) 855 | steam_folder_entry.insert(1, check_config("steam_folder", "")) 856 | game_folder_entry.insert(1, self.edit_destination_folder.get()) 857 | button_BOIII_browse.configure(command=open_game_browser) 858 | button_steam_browse.configure(command=open_steam_browser) 859 | copy_button.configure(command=start_copy_operation) 860 | cut_check.configure(command = lambda: check_status(cut_var, copy_var)) 861 | copy_check.configure(command = lambda: check_status(copy_var, cut_var)) 862 | main_app.app.create_context_menu(steam_folder_entry) 863 | main_app.app.create_context_menu(game_folder_entry) 864 | copy_var.set(True) 865 | progress_bar.set(0) 866 | top.after(150, top.focus_force) 867 | 868 | except Exception as e: 869 | print(f"[{get_current_datetime()}] [logs] error in workshop_to_gamedir_toplevel: {e}") 870 | show_message("Error", f"{e}", icon="cancel") 871 | 872 | def use_steam_creds_inputs(self): 873 | try: 874 | if self.use_steam_creds_sw.get() == 0: 875 | save_config("use_steam_creds", "off") 876 | save_config("login_cached", "off") 877 | return 878 | top = ctk.CTkToplevel(self) 879 | if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")): 880 | top.after(210, lambda: top.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico"))) 881 | _, _, x, y = get_window_size_from_registry() 882 | top.geometry(f"+{x}+{y}") 883 | top.title("Input your Steam Username") 884 | top.geometry("280x130") 885 | top.resizable(False, False) 886 | 887 | center_frame = ctk.CTkFrame(top) 888 | center_frame.pack(expand=True, fill=ctk.BOTH, padx=20, pady=20) 889 | 890 | username_label = ctk.CTkLabel(center_frame, text="Username:") 891 | username_label.grid(row=0, column=0, padx=10, pady=10, sticky='w') 892 | username_entry = ctk.CTkEntry(center_frame, width=200) 893 | username_entry.grid(row=0, column=1, padx=10, pady=10, sticky='e') 894 | 895 | config_username_value = load_steam_creds() 896 | 897 | if config_username_value: 898 | username_entry.insert(0, config_username_value) 899 | 900 | def save_creds(): 901 | username_value = username_entry.get() 902 | if username_value.strip(): 903 | save_config("use_steam_creds", "on") 904 | save_steam_creds(username_value) 905 | top.destroy() 906 | else: 907 | self.use_steam_creds.set(False) 908 | save_config("use_steam_creds", "off") 909 | save_config("login_cached", "off") 910 | top.destroy() 911 | 912 | save_button = ctk.CTkButton(center_frame, text="Save", command=save_creds) 913 | save_button.grid(row=2, column=0, columnspan=2, pady=20) 914 | top.after(150, top.focus_force) 915 | top.after(250, top.focus_force) 916 | top.protocol("WM_DELETE_WINDOW", save_creds) 917 | 918 | except Exception as e: 919 | print(f"Error: {e}") 920 | 921 | def toggle_console_window(self): 922 | if not self.checkbox_show_console.get(): 923 | self.console = False 924 | hide_console() 925 | save_config("console", "off") 926 | else: 927 | self.console = True 928 | save_config("console", "on") 929 | show_console() 930 | -------------------------------------------------------------------------------- /boiiiwd_package/src/shared_vars.py: -------------------------------------------------------------------------------- 1 | import src.main as main 2 | 3 | app = main.BOIIIWD() 4 | -------------------------------------------------------------------------------- /boiiiwd_package/src/update_window.py: -------------------------------------------------------------------------------- 1 | import src.shared_vars as main_app 2 | from src.imports import * 3 | from src.helpers import * 4 | 5 | 6 | def check_for_updates_func(window, ignore_up_todate=False): 7 | try: 8 | latest_version = get_latest_release_version() 9 | current_version = VERSION 10 | int_latest_version = int(latest_version.replace("v", "").replace(".", "")) 11 | int_current_version = int(current_version.replace("v", "").replace(".", "")) 12 | 13 | if latest_version and int_latest_version > int_current_version: 14 | 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) 15 | 16 | result = msg_box.get() 17 | 18 | if result == "View": 19 | webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest") 20 | 21 | if result == "Yes": 22 | update_window = UpdateWindow(window, LATEST_RELEASE_URL) 23 | update_window.start_update() 24 | 25 | if result == "No": 26 | return 27 | 28 | elif int_latest_version < int_current_version: 29 | if ignore_up_todate: 30 | return 31 | msg_box = CTkMessagebox(title="Up to Date!", message=f"Unreleased version!\nCurrent Version: {current_version}\nLatest Version: {latest_version}", option_1="Ok", sound=True) 32 | result = msg_box.get() 33 | elif int_latest_version == int_current_version: 34 | if ignore_up_todate: 35 | return 36 | msg_box = CTkMessagebox(title="Up to Date!", message="No Updates Available!", option_1="Ok", sound=True) 37 | result = msg_box.get() 38 | 39 | else: 40 | show_message("Error!", "An error occured while checking for updates!\nCheck your internet and try again") 41 | 42 | except Exception as e: 43 | show_message("Error", f"Error while checking for updates: \n{e}", icon="cancel") 44 | 45 | class UpdateWindow(ctk.CTkToplevel): 46 | def __init__(self, master, update_url): 47 | super().__init__(master) 48 | self.title("BOIIIWD Self-Updater") 49 | _, _, x, y = get_window_size_from_registry() 50 | try: self.geometry(f"400x150+{x}+{y}") 51 | except: self.geometry("400x150") 52 | if os.path.exists(os.path.join(RESOURCES_DIR, "ryuk.ico")): 53 | self.after(250, lambda: self.iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico"))) 54 | self.protocol("WM_DELETE_WINDOW", self.cancel_update) 55 | self.attributes('-topmost', 'true') 56 | 57 | self.columnconfigure(0, weight=1) 58 | self.columnconfigure(1, weight=1) 59 | self.rowconfigure(0, weight=1) 60 | self.rowconfigure(1, weight=1) 61 | 62 | self.label_download = ctk.CTkLabel(self, text="Starting...") 63 | self.label_download.grid(row=0, column=0, padx=30, pady=(10, 0), sticky="w") 64 | 65 | self.label_size = ctk.CTkLabel(self, text="Size: 0") 66 | self.label_size.grid(row=0, column=1, padx=30, pady=(10, 0), sticky="e") 67 | 68 | self.progress_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "progress_bar_fill_color") 69 | self.progress_bar = ctk.CTkProgressBar(self, mode="determinate", height=20, corner_radius=7, progress_color=self.progress_color) 70 | self.progress_bar.grid(row=1, column=0, columnspan=4, padx=30, pady=10, sticky="ew") 71 | self.progress_bar.set(0) 72 | 73 | self.progress_label = ctk.CTkLabel(self.progress_bar, text="0%", font=("Helvetica", 12), fg_color="transparent", height=0, width=0, corner_radius=0) 74 | self.progress_label.place(relx=0.5, rely=0.5, anchor="center") 75 | 76 | self.cancel_button = ctk.CTkButton(self, text="Cancel", command=self.cancel_update) 77 | self.cancel_button.grid(row=2, column=0, padx=30, pady=(0, 10), sticky="w") 78 | 79 | self.update_url = update_url 80 | self.total_size = None 81 | self.up_cancelled = False 82 | 83 | def update_progress_bar(self): 84 | try: 85 | update_dir = os.path.join(APPLICATION_PATH, UPDATER_FOLDER) 86 | response = requests.get(LATEST_RELEASE_URL, stream=True) 87 | response.raise_for_status() 88 | current_exe = sys.argv[0] 89 | program_name = os.path.basename(current_exe) 90 | new_exe = os.path.join(update_dir, "BOIIIWD.exe") 91 | 92 | if not os.path.exists(update_dir): 93 | os.makedirs(update_dir) 94 | 95 | self.progress_bar.set(0.0) 96 | self.total_size = int(response.headers.get('content-length', 0)) 97 | self.label_size.configure(text=f"Size: {convert_bytes_to_readable(self.total_size)}") 98 | zip_path = os.path.join(update_dir, "latest_version.zip") 99 | 100 | with open(zip_path, "wb") as file: 101 | downloaded_size = 0 102 | for chunk in response.iter_content(chunk_size=8192): 103 | if self.up_cancelled: 104 | break 105 | if chunk: 106 | file.write(chunk) 107 | downloaded_size += len(chunk) 108 | progress = int((downloaded_size / self.total_size) * 100) 109 | 110 | self.after(1, lambda p=progress: self.label_download.configure(text=f"Downloading update...")) 111 | self.after(1, lambda v=progress / 100.0: self.progress_bar.set(v)) 112 | self.after(1, lambda p=progress: self.progress_label.configure(text=f"{p}%")) 113 | 114 | if not self.up_cancelled: 115 | self.progress_bar.set(1.0) 116 | with zipfile.ZipFile(zip_path, "r") as zip_ref: 117 | zip_ref.extractall(update_dir) 118 | self.label_download.configure(text="Update Downloaded successfully!") 119 | def update_msg(): 120 | msg = CTkMessagebox(title="Success!", message="Update Downloaded successfully!\nPress ok to install it", icon="info", option_1="No", option_2="Ok", sound=True) 121 | response = msg.get() 122 | if response == "No": 123 | self.destroy() 124 | return 125 | elif response == "Ok": 126 | script_path = create_update_script(current_exe, new_exe, update_dir, program_name) 127 | subprocess.run(('cmd', '/C', 'start', '', fr'{script_path}')) 128 | sys.exit(0) 129 | else: 130 | return 131 | self.after(0, update_msg) 132 | return 133 | else: 134 | if os.path.exists(zip_path): 135 | os.remove(fr"{zip_path}") 136 | self.label_download.configure(text="Update cancelled.") 137 | self.progress_bar.set(0.0) 138 | 139 | try: main_app.app.attributes('-alpha', 1.0) 140 | except: pass 141 | show_message("Cancelled!", "Update cancelled by user", icon="warning") 142 | 143 | except Exception as e: 144 | self.progress_bar.set(0.0) 145 | self.label_download.configure(text="Update failed") 146 | show_message("Error", f"Error installing the update\n{e}", icon="cancel") 147 | 148 | def start_update(self): 149 | self.thread = threading.Thread(target=self.update_progress_bar) 150 | self.thread.start() 151 | 152 | def cancel_update(self): 153 | self.up_cancelled = True 154 | self.withdraw() 155 | -------------------------------------------------------------------------------- /boiiiwd_package/src/winpty_patch.py: -------------------------------------------------------------------------------- 1 | # Monkey patching winpty to add a custom non-blocking output reader function (nb_read) 2 | # This function uses select to avoid blocking if there's no data to read. 3 | 4 | import select 5 | from winpty import PtyProcess as OriginalPtyProcess 6 | 7 | 8 | class PatchedPtyProcess(OriginalPtyProcess): 9 | def nb_read(self, size: int = 1024) -> str: 10 | """Read and return at most ``size`` characters from the pty. 11 | 12 | Uses select to avoid blocking if there's no data to read. 13 | Raises :exc:`EOFError` if the terminal was closed. 14 | """ 15 | ... 16 | 17 | 18 | def nb_read(self, size=1024): 19 | """Read and return at most ``size`` characters from the pty. 20 | 21 | Uses select to avoid blocking if there's no data to read. 22 | Raises :exc:`EOFError` if the terminal was closed. 23 | """ 24 | r, _, _ = select.select([self.fileobj], [], [], 0) 25 | if not r: 26 | return "" 27 | 28 | data = self.fileobj.recv(size) 29 | if not data: 30 | self.flag_eof = True 31 | raise EOFError("Pty is closed") 32 | 33 | if data == b"0011Ignore": 34 | data = "" 35 | 36 | err = True 37 | while err and data: 38 | try: 39 | data.decode("utf-8") 40 | err = False 41 | except UnicodeDecodeError: 42 | data += self.fileobj.recv(1) 43 | return data.decode("utf-8") 44 | 45 | 46 | OriginalPtyProcess.nb_read = nb_read 47 | PtyProcess: PatchedPtyProcess = OriginalPtyProcess 48 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | import PyInstaller.__main__ 2 | 3 | NAME = "BOIIIWD" 4 | SCRIPT = "boiiiwd_package/boiiiwd.py" 5 | ICON = "boiiiwd_package/resources/ryuk.ico" 6 | 7 | PyInstaller.__main__.run([ 8 | f"{SCRIPT}", 9 | '--name', f"{NAME}", 10 | "--noconfirm", 11 | "--onefile", 12 | "--noconsole", 13 | "--icon", f"{ICON}", 14 | "--add-data", "boiiiwd_package/resources:resources", 15 | "--add-data", "boiiiwd_package/src:imports", 16 | "--add-data", "boiiiwd_package/src:winpty_patch", 17 | "--add-data", "boiiiwd_package/src:helpers", 18 | "--add-data", "boiiiwd_package/src:shared_vars", 19 | "--add-data", "boiiiwd_package/src:library_tab", 20 | "--add-data", "boiiiwd_package/src:settings_tab", 21 | "--add-data", "boiiiwd_package/src:update_window", 22 | "--add-data", "boiiiwd_package/src:main", 23 | "--distpath", ".", 24 | ]) 25 | -------------------------------------------------------------------------------- /md/LOGIN.md: -------------------------------------------------------------------------------- 1 | # BOIIIWD Login Tutorial: 2 | Follow these steps to log in and get started with BOIIIWD: 3 | 4 | 1. **Open the Application**: 5 | - Launch BOIIIWD.exe or run the script. 6 | 7 | 2. **Go to Settings Tab**: 8 | - Navigate to the Settings tab from the UI. 9 | - Enable the "Use Steam Credentials" option. 10 | - ![Settings Screenshot](https://github.com/user-attachments/assets/b4e163c9-ff67-4c7e-9436-dbb05e130862) 11 | 12 | 3. **Start Your Download**: 13 | - Begin the download process for your desired mods and maps. 14 | 15 | 4. **First Time Login**: 16 | - If it's your first time, a separate console window from SteamCMD will launch to prompt you for your password and Steam Guard code. 17 | - ![boiiiwd_s3](https://github.com/user-attachments/assets/7b88c01f-3ff0-4691-b202-0345e372ea54) 18 | - If the login fails, it will go back to "Steam>". You can type `login ` to relaunch the login process. 19 | - ![boiiiwd_s1](https://github.com/user-attachments/assets/0439eba9-cd56-4c5a-9019-5644a655e5ae) 20 | - Once you successfully log in and see "Steam>" again, close the console window. 21 | 22 | 5. **Proceed with Download**: 23 | - The application will proceed with the download using your cached Steam login. 24 | - **Note**: Nothing related to passwords or tokens is stored by BOIIIWD. Login is only handled by SteamCMD. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faroukbmiled/BOIIIWD/23569a78068473eed6333c1c4e9f5ae94a97de6f/requirements.txt -------------------------------------------------------------------------------- /utils/enc_key_gen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | 4 | def generate_key(): 5 | key = os.urandom(32) 6 | print(f"Generated AES key: {base64.b64encode(key).decode('utf-8')}") 7 | 8 | 9 | if __name__ == "__main__": 10 | generate_key() 11 | --------------------------------------------------------------------------------