├── .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 |
7 |
8 | |
9 |
10 |
11 | |
12 |
13 |
14 | |
15 |
16 |
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 | - 
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 | - 
18 | - If the login fails, it will go back to "Steam>". You can type `login ` to relaunch the login process.
19 | - 
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 |
--------------------------------------------------------------------------------