1
+ import sys
2
+ import subprocess
3
+ import ctypes
4
+ import os
5
+ import time
6
+ from PyQt6 .QtWidgets import (QApplication , QMainWindow , QWidget , QVBoxLayout ,
7
+ QHBoxLayout , QCheckBox , QPushButton , QLabel ,
8
+ QGridLayout , QFrame , QProgressBar )
9
+ from PyQt6 .QtCore import Qt , QThread , pyqtSignal
10
+ from PyQt6 .QtGui import QFont , QColor
11
+
12
+ def is_admin ():
13
+ try :
14
+ return ctypes .windll .shell32 .IsUserAnAdmin ()
15
+ except :
16
+ return False
17
+
18
+ def run_as_admin (command ):
19
+ if not is_admin ():
20
+ ctypes .windll .shell32 .ShellExecuteW (None , "runas" , "cmd.exe" , f"/c { command } " , None , 1 )
21
+ return True
22
+ return False
23
+
24
+ def run_without_admin (command ):
25
+ batch_content = f"""
26
+ @echo off
27
+ set command={ command }
28
+ start cmd /c "%command%"
29
+ """
30
+ with open ('temp_command.bat' , 'w' , encoding = 'utf-8' ) as f :
31
+ f .write (batch_content )
32
+
33
+ subprocess .run (['explorer.exe' , 'temp_command.bat' ], shell = True )
34
+
35
+ try :
36
+ time .sleep (1 )
37
+ os .remove ('temp_command.bat' )
38
+ except :
39
+ pass
40
+
41
+ class PackageManagerThread (QThread ):
42
+ progress_update = pyqtSignal (str )
43
+ progress_value = pyqtSignal (int )
44
+
45
+ def __init__ (self , apps_to_process , action = "install" ):
46
+ super ().__init__ ()
47
+ self .apps_to_process = apps_to_process
48
+ self .action = action
49
+ self .total_apps = len (apps_to_process )
50
+ self .current_app = 0
51
+
52
+ def update_progress (self ):
53
+ self .current_app += 1
54
+ progress = int ((self .current_app / self .total_apps ) * 100 )
55
+ self .progress_value .emit (progress )
56
+
57
+ def run (self ):
58
+ self .current_app = 0
59
+ self .progress_value .emit (0 )
60
+
61
+ spotify_app = next ((app for app in self .apps_to_process if app ["name" ] == "Spotify" ), None )
62
+ other_apps = [app for app in self .apps_to_process if app ["name" ] != "Spotify" ]
63
+
64
+ if spotify_app and self .action == "install" :
65
+ self .progress_update .emit (f"Installing { spotify_app ['name' ]} ..." )
66
+ try :
67
+ command = f'winget install -e -h --id { spotify_app ["id" ]} --accept-source-agreements --accept-package-agreements'
68
+ run_without_admin (command )
69
+ self .progress_update .emit (f"Started { spotify_app ['name' ]} installation" )
70
+ except Exception as e :
71
+ self .progress_update .emit (f"Failed to install { spotify_app ['name' ]} : { str (e )} " )
72
+ self .update_progress ()
73
+
74
+ for app in other_apps :
75
+ action_text = "Installing" if self .action == "install" else "Uninstalling"
76
+ self .progress_update .emit (f"{ action_text } { app ['name' ]} ..." )
77
+
78
+ try :
79
+ if self .action == "install" :
80
+ command = f'winget install -e -h --id { app ["id" ]} --silent --accept-source-agreements --accept-package-agreements'
81
+ else :
82
+ command = f'winget uninstall -e -h --id { app ["id" ]} --silent'
83
+
84
+ if not is_admin ():
85
+ run_as_admin (command )
86
+ else :
87
+ subprocess .run (command , shell = True , check = True )
88
+
89
+ action_text = "installed" if self .action == "install" else "uninstalled"
90
+ self .progress_update .emit (f"Successfully { action_text } { app ['name' ]} " )
91
+ except subprocess .CalledProcessError :
92
+ action_text = "install" if self .action == "install" else "uninstall"
93
+ self .progress_update .emit (f"Failed to { action_text } { app ['name' ]} " )
94
+
95
+ self .update_progress ()
96
+
97
+ class AppCard (QFrame ):
98
+ def __init__ (self , app_name , parent = None ):
99
+ super ().__init__ (parent )
100
+ self .setObjectName ("AppCard" )
101
+
102
+ layout = QHBoxLayout (self )
103
+ layout .setContentsMargins (16 , 12 , 16 , 12 )
104
+ layout .setSpacing (12 )
105
+
106
+ self .checkbox = QCheckBox (app_name )
107
+ self .checkbox .setFont (QFont ("Segoe UI" , 11 ))
108
+ layout .addWidget (self .checkbox )
109
+
110
+ self .checkbox .toggled .connect (self .update_selection )
111
+
112
+ def update_selection (self , checked ):
113
+ self .setProperty ("selected" , checked )
114
+ self .style ().unpolish (self )
115
+ self .style ().polish (self )
116
+ self .update ()
117
+
118
+ class ModernInstaller (QMainWindow ):
119
+ def __init__ (self ):
120
+ super ().__init__ ()
121
+ self .setWindowTitle ("Modular" )
122
+ self .setMinimumSize (900 , 600 )
123
+ self .setup_ui ()
124
+
125
+ def setup_ui (self ):
126
+ self .apps = [
127
+ {"name" : "Spotify" , "id" : "Spotify.Spotify" },
128
+ {"name" : "Discord" , "id" : "Discord.Discord" },
129
+ {"name" : "Brave" , "id" : "Brave.Brave" },
130
+ {"name" : "Steam" , "id" : "Valve.Steam" },
131
+ {"name" : "7-Zip" , "id" : "7zip.7zip" },
132
+ {"name" : "Stremio" , "id" : "Stremio.Stremio" },
133
+ {"name" : "JPEGView" , "id" : "JPEGView.JPEGView" },
134
+ {"name" : "Visual C++ Redistributable" , "id" : "Microsoft.VCRedist.2015+.x64" },
135
+ {"name" : "Git" , "id" : "Git.Git" },
136
+ {"name" : "Python 3.10" , "id" : "Python.Python.3.10" },
137
+ {"name" : "Bitwarden" , "id" : "Bitwarden.Bitwarden" },
138
+ {"name" : "Mullvad VPN" , "id" : "MullvadVPN.MullvadVPN" },
139
+ {"name" : "Process Explorer" , "id" : "Microsoft.Sysinternals.ProcessExplorer" },
140
+ {"name" : "Visual Studio Code" , "id" : "Microsoft.VisualStudioCode" },
141
+ {"name" : "Trading View" , "id" : "TradingView.TradingViewDesktop" }
142
+ ]
143
+
144
+ central_widget = QWidget ()
145
+ self .setCentralWidget (central_widget )
146
+ main_layout = QVBoxLayout (central_widget )
147
+ main_layout .setContentsMargins (30 , 30 , 30 , 30 )
148
+ main_layout .setSpacing (25 )
149
+
150
+ title = QLabel ("Select applications to install" )
151
+ title .setFont (QFont ("Segoe UI" , 32 , QFont .Weight .Light ))
152
+ main_layout .addWidget (title )
153
+
154
+ grid_widget = QWidget ()
155
+ grid_layout = QGridLayout (grid_widget )
156
+ grid_layout .setSpacing (10 )
157
+ grid_layout .setContentsMargins (0 , 0 , 0 , 0 )
158
+
159
+ self .app_cards = {}
160
+ row = 0
161
+ col = 0
162
+ max_cols = 3
163
+
164
+ for app in self .apps :
165
+ card = AppCard (app ["name" ])
166
+ self .app_cards [app ["name" ]] = card
167
+ grid_layout .addWidget (card , row , col )
168
+
169
+ col += 1
170
+ if col >= max_cols :
171
+ col = 0
172
+ row += 1
173
+
174
+ main_layout .addWidget (grid_widget )
175
+
176
+ progress_container = QWidget ()
177
+ progress_layout = QVBoxLayout (progress_container )
178
+ progress_layout .setSpacing (8 )
179
+ progress_layout .setContentsMargins (0 , 0 , 0 , 0 )
180
+
181
+ self .progress_label = QLabel ("Ready to install" )
182
+ self .progress_label .setFont (QFont ("Segoe UI" , 11 ))
183
+ self .progress_label .setAlignment (Qt .AlignmentFlag .AlignCenter )
184
+ progress_layout .addWidget (self .progress_label )
185
+
186
+ self .progress_bar = QProgressBar ()
187
+ self .progress_bar .setTextVisible (False )
188
+ self .progress_bar .setFixedHeight (4 )
189
+ self .progress_bar .setMaximum (100 )
190
+ self .progress_bar .setValue (0 )
191
+ progress_layout .addWidget (self .progress_bar )
192
+
193
+ main_layout .addWidget (progress_container )
194
+
195
+ button_layout = QHBoxLayout ()
196
+ button_layout .setSpacing (8 )
197
+
198
+ select_all_btn = QPushButton ("Select All" )
199
+ select_all_btn .clicked .connect (self .select_all )
200
+ button_layout .addWidget (select_all_btn )
201
+
202
+ clear_btn = QPushButton ("Clear All" )
203
+ clear_btn .clicked .connect (self .clear_all )
204
+ button_layout .addWidget (clear_btn )
205
+
206
+ button_layout .addStretch ()
207
+
208
+ uninstall_btn = QPushButton ("Uninstall Selected" )
209
+ uninstall_btn .clicked .connect (lambda : self .process_selected ("uninstall" ))
210
+ button_layout .addWidget (uninstall_btn )
211
+
212
+ install_btn = QPushButton ("Install Selected" )
213
+ install_btn .clicked .connect (lambda : self .process_selected ("install" ))
214
+ install_btn .setProperty ("primary" , True )
215
+ button_layout .addWidget (install_btn )
216
+
217
+ main_layout .addLayout (button_layout )
218
+
219
+ self .setStyleSheet ("""
220
+ QMainWindow {
221
+ background-color: #1f1f1f;
222
+ }
223
+ QWidget {
224
+ background-color: transparent;
225
+ color: #ffffff;
226
+ }
227
+ #AppCard {
228
+ background-color: rgba(255, 255, 255, 0.04);
229
+ border: 1px solid rgba(255, 255, 255, 0.1);
230
+ border-radius: 6px;
231
+ }
232
+ #AppCard:hover {
233
+ background-color: rgba(255, 255, 255, 0.08);
234
+ border: 1px solid rgba(255, 255, 255, 0.15);
235
+ }
236
+ #AppCard[selected="true"] {
237
+ background-color: rgba(0, 120, 212, 0.2);
238
+ border: 1px solid #0078d4;
239
+ }
240
+ QCheckBox {
241
+ spacing: 12px;
242
+ }
243
+ QCheckBox::indicator {
244
+ width: 20px;
245
+ height: 20px;
246
+ border-radius: 4px;
247
+ border: 1px solid rgba(255, 255, 255, 0.3);
248
+ background-color: transparent;
249
+ }
250
+ QCheckBox::indicator:hover {
251
+ border: 1px solid rgba(255, 255, 255, 0.5);
252
+ background-color: rgba(255, 255, 255, 0.1);
253
+ }
254
+ QCheckBox::indicator:checked {
255
+ background-color: #0078d4;
256
+ border: 1px solid #0078d4;
257
+ }
258
+ QCheckBox::indicator:checked:hover {
259
+ background-color: #1884d7;
260
+ border: 1px solid #1884d7;
261
+ }
262
+ QPushButton {
263
+ background-color: rgba(255, 255, 255, 0.06);
264
+ border: none;
265
+ padding: 8px 20px;
266
+ border-radius: 4px;
267
+ font-family: 'Segoe UI';
268
+ font-size: 13px;
269
+ min-width: 100px;
270
+ min-height: 32px;
271
+ }
272
+ QPushButton:hover {
273
+ background-color: rgba(255, 255, 255, 0.08);
274
+ }
275
+ QPushButton[primary="true"] {
276
+ background-color: #0078d4;
277
+ }
278
+ QPushButton[primary="true"]:hover {
279
+ background-color: #1884d7;
280
+ }
281
+ QLabel {
282
+ color: white;
283
+ }
284
+ QProgressBar {
285
+ background-color: rgba(255, 255, 255, 0.1);
286
+ border: none;
287
+ border-radius: 2px;
288
+ }
289
+ QProgressBar::chunk {
290
+ background-color: #0078d4;
291
+ border-radius: 2px;
292
+ }
293
+ """ )
294
+
295
+ def select_all (self ):
296
+ for card in self .app_cards .values ():
297
+ card .checkbox .setChecked (True )
298
+
299
+ def clear_all (self ):
300
+ for card in self .app_cards .values ():
301
+ card .checkbox .setChecked (False )
302
+
303
+ def process_selected (self , action ):
304
+ selected_apps = [
305
+ app for app in self .apps
306
+ if self .app_cards [app ["name" ]].checkbox .isChecked ()
307
+ ]
308
+
309
+ if not selected_apps :
310
+ self .progress_label .setText ("Please select at least one application" )
311
+ return
312
+
313
+ for card in self .app_cards .values ():
314
+ card .checkbox .setEnabled (False )
315
+
316
+ self .progress_bar .setValue (0 )
317
+
318
+ self .manager_thread = PackageManagerThread (selected_apps , action )
319
+ self .manager_thread .progress_update .connect (self .update_progress )
320
+ self .manager_thread .progress_value .connect (self .progress_bar .setValue )
321
+ self .manager_thread .finished .connect (lambda : self .process_finished (selected_apps ))
322
+ self .manager_thread .start ()
323
+
324
+ def update_progress (self , message ):
325
+ self .progress_label .setText (message )
326
+
327
+ def process_finished (self , processed_apps ):
328
+ for card in self .app_cards .values ():
329
+ card .checkbox .setEnabled (True )
330
+ if any (app ["name" ] == card .checkbox .text () for app in processed_apps ):
331
+ card .checkbox .setChecked (False )
332
+ self .progress_label .setText ("Operation completed!" )
333
+
334
+ def main ():
335
+ if not is_admin ():
336
+ ctypes .windll .shell32 .ShellExecuteW (None , "runas" , sys .executable , " " .join (sys .argv ), None , 1 )
337
+ sys .exit ()
338
+
339
+ app = QApplication (sys .argv )
340
+ window = ModernInstaller ()
341
+ window .show ()
342
+ sys .exit (app .exec ())
343
+
344
+ if __name__ == "__main__" :
345
+ main ()
0 commit comments