-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpicodulce.py
1820 lines (1495 loc) · 76.1 KB
/
picodulce.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import sys
import subprocess
import threading
from threading import Thread
import logging
import re
import shutil
import platform
import requests
import json
import os
import time
from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QMovie, QPixmap, QDesktopServices, QBrush
from PyQt5.QtCore import Qt, QObject, pyqtSignal, QThread, QUrl, QMetaObject, Q_ARG, QByteArray, QSize
from datetime import datetime
logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
class PicomcVersionSelector(QWidget):
def __init__(self):
self.current_state = "menu"
self.open_dialogs = []
self.check_config_file()
self.themes_integrity()
themes_folder = "themes"
theme_file = self.config.get("Theme", "Dark.json")
# Ensure the theme file exists in the themes directory
theme_file_path = os.path.join(themes_folder, theme_file)
try:
# Load and apply the theme from the file
self.load_theme_from_file(theme_file_path, app)
print(f"Theme '{theme_file}' loaded successfully.")
except Exception as e:
print(f"Error: Could not load theme '{theme_file}'. Falling back to default theme. {e}")
super().__init__()
self.init_ui()
if self.config.get("CheckUpdate", False):
self.check_for_update_start()
if self.config.get("IsRCPenabled", False):
discord_rcp_thread = Thread(target=self.start_discord_rcp)
discord_rcp_thread.daemon = True # Make the thread a daemon so it terminates when the main program exits
discord_rcp_thread.start()
if self.config.get("IsFirstLaunch", False):
self.FirstLaunch()
def load_theme_from_file(self, file_path, app):
self.theme = {}
# Check if the file exists, else load 'Dark.json'
if not os.path.exists(file_path):
print(f"Theme file '{file_path}' not found. Loading default 'Dark.json' instead.")
file_path = "themes/Dark.json"
# Ensure the fallback file exists
if not os.path.exists(file_path):
raise FileNotFoundError(f"Default theme file '{file_path}' not found.")
# Open and parse the JSON file
with open(file_path, "r") as file:
self.theme = json.load(file) # Store theme as a class attribute
# Ensure the required keys exist
if "palette" not in self.theme:
raise ValueError("JSON theme must contain a 'palette' section.")
# Extract the palette
palette_config = self.theme["palette"]
# Create a new QPalette
palette = QPalette()
# Map palette roles to PyQt5 palette roles
role_map = {
"Window": QPalette.Window,
"WindowText": QPalette.WindowText,
"Base": QPalette.Base,
"AlternateBase": QPalette.AlternateBase,
"ToolTipBase": QPalette.ToolTipBase,
"ToolTipText": QPalette.ToolTipText,
"Text": QPalette.Text,
"Button": QPalette.Button,
"ButtonText": QPalette.ButtonText,
"BrightText": QPalette.BrightText,
"Link": QPalette.Link,
"Highlight": QPalette.Highlight,
"HighlightedText": QPalette.HighlightedText,
}
# Apply colors from the palette config
for role_name, color_code in palette_config.items():
if role_name in role_map:
palette.setColor(role_map[role_name], QColor(color_code))
else:
print(f"Warning: '{role_name}' is not a recognized palette role.")
# Apply the palette to the application
app.setPalette(palette)
# Apply style sheet if present
if "stylesheet" in self.theme:
stylesheet = self.theme["stylesheet"]
app.setStyleSheet(stylesheet)
else:
print("Theme dosn't seem to have a stylesheet")
def themes_integrity(self):
# Define folder and file paths
themes_folder = "themes"
dark_theme_file = os.path.join(themes_folder, "Dark.json")
native_theme_file = os.path.join(themes_folder, "Native.json")
# Define the default content for Dark.json
dark_theme_content = {
"manifest": {
"name": "Dark",
"description": "The default picodulce launcher theme",
"author": "Nixietab",
"license": "MIT"
},
"palette": {
"Window": "#353535",
"WindowText": "#ffffff",
"Base": "#191919",
"AlternateBase": "#353535",
"ToolTipBase": "#ffffff",
"ToolTipText": "#ffffff",
"Text": "#ffffff",
"Button": "#353535",
"ButtonText": "#ffffff",
"BrightText": "#ff0000",
"Link": "#2a82da",
"Highlight": "#4bb679",
"HighlightedText": "#ffffff"
},
"background_image_base64": ""
}
# Define the default content for Native.json
native_theme_content = {
"manifest": {
"name": "Native",
"description": "The native looks of your OS",
"author": "Your Qt Style",
"license": "Any"
},
"palette": {}
}
# Step 1: Ensure the themes folder exists
if not os.path.exists(themes_folder):
print(f"Creating folder: {themes_folder}")
os.makedirs(themes_folder)
# Step 2: Ensure Dark.json exists
if not os.path.isfile(dark_theme_file):
print(f"Creating file: {dark_theme_file}")
with open(dark_theme_file, "w", encoding="utf-8") as file:
json.dump(dark_theme_content, file, indent=2)
print("Dark.json has been created successfully.")
# Step 3: Ensure Native.json exists
if not os.path.isfile(native_theme_file):
print(f"Creating file: {native_theme_file}")
with open(native_theme_file, "w", encoding="utf-8") as file:
json.dump(native_theme_content, file, indent=2)
print("Native.json has been created successfully.")
# Check if both files exist and print OK message
if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file):
print("Theme Integrity OK")
def FirstLaunch(self):
try:
self.config_path = "config.json"
print("Running picomc instance create default command...")
# Run the command using subprocess
result = subprocess.run(["picomc", "instance", "create", "default"], check=True, capture_output=True, text=True)
# Print the output of the command
print("Command output:", result.stdout)
# Change the value of IsFirstLaunch to False
self.config["IsFirstLaunch"] = False
print("IsFirstLaunch set to False")
# Save the updated config to the config.json file
with open(self.config_path, 'w') as f:
json.dump(self.config, f, indent=4)
print("Configuration saved to", self.config_path)
except subprocess.CalledProcessError as e:
print("An error occurred while creating the instance.")
print("Error output:", e.stderr)
def resize_event(self, event):
if hasattr(self, 'movie_label'):
self.movie_label.setGeometry(0, 0, 400, 320)
event.accept() # Accept the resize event
def init_ui(self):
self.setWindowTitle('PicoDulce Launcher') # Change window title
current_date = datetime.now()
if (current_date.month == 12 and current_date.day >= 8) or (current_date.month == 1 and current_date.day <= 1):
self.setWindowIcon(QIcon('holiday.ico')) # Set holiday icon
else:
self.setWindowIcon(QIcon('launcher_icon.ico')) # Set regular icon
self.setGeometry(100, 100, 400, 250)
# Set application style and theme
QApplication.setStyle("Fusion")
with open("config.json", "r") as config_file:
config = json.load(config_file)
if self.config.get("ThemeBackground", False): # Default to False if ThemeBackground is missing
# Get the base64 string for the background image from the theme file
theme_background_base64 = self.theme.get("background_image_base64", "")
if theme_background_base64:
try:
# Decode the base64 string to get the binary data
background_image_data = QByteArray.fromBase64(theme_background_base64.encode())
temp_gif_path = "temp.gif" # Write the gif into a temp file because Qt stuff
with open(temp_gif_path, 'wb') as temp_gif_file:
temp_gif_file.write(background_image_data)
# Create a QMovie object from the temporary file
movie = QMovie(temp_gif_path)
if movie.isValid():
self.setAutoFillBackground(True)
palette = self.palette()
# Set the QMovie to a QLabel
self.movie_label = QLabel(self)
self.movie_label.setMovie(movie)
self.movie_label.setGeometry(0, 0, movie.frameRect().width(), movie.frameRect().height())
self.movie_label.setScaledContents(True) # Ensure the QLabel scales its contents
movie.start()
# Use the QLabel pixmap as the brush texture
brush = QBrush(QPixmap(movie.currentPixmap()))
brush.setStyle(Qt.TexturePattern)
palette.setBrush(QPalette.Window, brush)
self.setPalette(palette)
# Adjust the QLabel size when the window is resized
self.movie_label.resizeEvent = self.resize_event
else:
print("Error: Failed to load background GIF from base64 string.")
except Exception as e:
print(f"Error: Failed to decode and set background GIF. {e}")
else:
print("No background GIF base64 string found in the theme file.")
# Create title label
title_label = QLabel('PicoDulce Launcher') # Change label text
title_label.setFont(QFont("Arial", 24, QFont.Bold))
# Create installed versions section
installed_versions_label = QLabel('Installed Versions:')
installed_versions_label.setFont(QFont("Arial", 14))
self.installed_version_combo = QComboBox()
self.installed_version_combo.setMinimumWidth(200)
self.populate_installed_versions()
# Create buttons layout
buttons_layout = QVBoxLayout()
# Create play button for installed versions
self.play_button = QPushButton('Play')
self.play_button.clicked.connect(self.play_instance)
highlight_color = self.palette().color(QPalette.Highlight)
self.play_button.setStyleSheet(f"background-color: {highlight_color.name()}; color: white;")
buttons_layout.addWidget(self.play_button)
# Version Manager Button
self.open_menu_button = QPushButton('Version Manager')
self.open_menu_button.clicked.connect(self.open_mod_loader_and_version_menu)
buttons_layout.addWidget(self.open_menu_button)
# Create button to manage accounts
self.manage_accounts_button = QPushButton('Manage Accounts')
self.manage_accounts_button.clicked.connect(self.manage_accounts)
buttons_layout.addWidget(self.manage_accounts_button)
# Create a button for the marroc mod loader
self.open_marroc_button = QPushButton('Marroc Mod Manager')
self.open_marroc_button.clicked.connect(self.open_marroc_script)
buttons_layout.addWidget(self.open_marroc_button)
# Create grid layout for Settings and About buttons
grid_layout = QGridLayout()
self.settings_button = QPushButton('Settings')
self.settings_button.clicked.connect(self.open_settings_dialog)
self.about_button = QPushButton('About')
self.about_button.clicked.connect(self.show_about_dialog)
grid_layout.addWidget(self.settings_button, 0, 0)
grid_layout.addWidget(self.about_button, 0, 1)
# Add the grid layout to buttons layout
buttons_layout.addLayout(grid_layout)
# Set buttons layout alignment and spacing
buttons_layout.setAlignment(Qt.AlignTop)
buttons_layout.setSpacing(10)
# Set main layout
main_layout = QVBoxLayout()
main_layout.addWidget(title_label, alignment=Qt.AlignCenter)
main_layout.addWidget(installed_versions_label)
main_layout.addWidget(self.installed_version_combo)
main_layout.addLayout(buttons_layout)
main_layout.setAlignment(Qt.AlignCenter)
main_layout.setSpacing(20)
self.setLayout(main_layout)
def keyPressEvent(self, event):
focus_widget = self.focusWidget()
if event.key() == Qt.Key_Down:
self.focusNextChild() # Move focus to the next widget
elif event.key() == Qt.Key_Up:
self.focusPreviousChild() # Move focus to the previous widget
elif event.key() in [Qt.Key_Return, Qt.Key_Enter]:
if isinstance(focus_widget, QPushButton):
focus_widget.click() # Trigger the button click
elif isinstance(focus_widget, QComboBox):
focus_widget.showPopup() # Show dropdown for combo box
else:
super().keyPressEvent(event)
def check_config_file(self):
config_path = "config.json"
default_config = {
"IsRCPenabled": False,
"CheckUpdate": False,
"IsBleeding": False,
"LastPlayed": "",
"IsFirstLaunch": True,
"Instance": "default",
"Theme": "Dark.json",
"ThemeBackground": True,
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json"
}
# Step 1: Check if the file exists; if not, create it with default values
if not os.path.exists(config_path):
with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4)
self.config = default_config
return
# Step 2: Try loading the config file, handle invalid JSON
try:
with open(config_path, "r") as config_file:
self.config = json.load(config_file)
except (json.JSONDecodeError, ValueError):
# File is corrupted, overwrite it with default configuration
with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4)
self.config = default_config
return
# Step 3: Check for missing keys and add defaults if necessary
updated = False
for key, value in default_config.items():
if key not in self.config: # Field is missing
self.config[key] = value
updated = True
# Step 4: Save the repaired config back to the file
if updated:
with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4)
def open_settings_dialog(self):
dialog = QDialog(self)
dialog.setWindowTitle('Settings')
# Make the window resizable
dialog.setMinimumSize(400, 300)
# Create a Tab Widget
tab_widget = QTabWidget()
# Create the Settings Tab
settings_tab = QWidget()
settings_layout = QVBoxLayout()
title_label = QLabel('Settings')
title_label.setFont(QFont("Arial", 14))
# Create checkboxes for settings tab
discord_rcp_checkbox = QCheckBox('Discord Rich Presence')
discord_rcp_checkbox.setChecked(self.config.get("IsRCPenabled", False))
check_updates_checkbox = QCheckBox('Check Updates on Start')
check_updates_checkbox.setChecked(self.config.get("CheckUpdate", False))
bleeding_edge_checkbox = QCheckBox('Bleeding Edge')
bleeding_edge_checkbox.setChecked(self.config.get("IsBleeding", False))
bleeding_edge_checkbox.stateChanged.connect(lambda: self.show_bleeding_edge_popup(bleeding_edge_checkbox))
settings_layout.addWidget(title_label)
settings_layout.addWidget(discord_rcp_checkbox)
settings_layout.addWidget(check_updates_checkbox)
settings_layout.addWidget(bleeding_edge_checkbox)
# Add buttons in the settings tab
update_button = QPushButton('Check for updates')
update_button.clicked.connect(self.check_for_update)
open_game_directory_button = QPushButton('Open game directory')
open_game_directory_button.clicked.connect(self.open_game_directory)
stats_button = QPushButton('Stats for Nerds')
stats_button.clicked.connect(self.show_system_info)
settings_layout.addWidget(update_button)
settings_layout.addWidget(open_game_directory_button)
settings_layout.addWidget(stats_button)
settings_tab.setLayout(settings_layout)
# Create the Customization Tab
customization_tab = QWidget()
customization_layout = QVBoxLayout()
# Create theme background checkbox for customization tab
theme_background_checkbox = QCheckBox('Theme Background')
theme_background_checkbox.setChecked(self.config.get("ThemeBackground", False))
# Label to show currently selected theme
theme_filename = self.config.get('Theme', 'Dark.json')
current_theme_label = QLabel(f"Current Theme: {theme_filename}")
# QListWidget to display available themes
json_files_label = QLabel('Installed Themes:')
json_files_list_widget = QListWidget()
# Track selected theme
self.selected_theme = theme_filename # Default to current theme
# Populate themes initially
self.populate_themes(json_files_list_widget)
# Update current theme label when a theme is selected
json_files_list_widget.itemClicked.connect(
lambda: self.on_theme_selected(json_files_list_widget, current_theme_label)
)
# Add widgets to the layout
customization_layout.addWidget(theme_background_checkbox)
customization_layout.addWidget(current_theme_label)
customization_layout.addWidget(json_files_label)
customization_layout.addWidget(json_files_list_widget)
# Button to download themes
download_themes_button = QPushButton("Download More Themes")
download_themes_button.clicked.connect(self.download_themes_window)
customization_layout.addWidget(download_themes_button)
customization_tab.setLayout(customization_layout)
# Add the tabs to the TabWidget
tab_widget.addTab(settings_tab, "Settings")
tab_widget.addTab(customization_tab, "Customization")
# Save button
save_button = QPushButton('Save')
save_button.clicked.connect(
lambda: self.save_settings(
discord_rcp_checkbox.isChecked(),
check_updates_checkbox.isChecked(),
theme_background_checkbox.isChecked(),
self.selected_theme, # Pass the selected theme here
bleeding_edge_checkbox.isChecked() # Pass the bleeding edge setting here
)
)
# Main layout
main_layout = QVBoxLayout()
main_layout.addWidget(tab_widget)
main_layout.addWidget(save_button)
dialog.setLayout(main_layout)
dialog.exec_()
def show_bleeding_edge_popup(self, checkbox):
if checkbox.isChecked():
response = QMessageBox.question(
self,
"Bleeding Edge Feature",
"Enabling 'Bleeding Edge' mode may expose you to unstable and experimental features. Do you want to enable it anyway? In normal mode, updates are only downloaded when a stable release is made.",
QMessageBox.Yes | QMessageBox.No
)
if response == QMessageBox.No:
checkbox.setChecked(False)
def populate_themes(self, json_files_list_widget):
themes_folder = os.path.join(os.getcwd(), "themes")
json_files_list_widget.clear()
if os.path.exists(themes_folder):
json_files = [f for f in os.listdir(themes_folder) if f.endswith('.json')]
for json_file in json_files:
json_path = os.path.join(themes_folder, json_file)
with open(json_path, 'r') as file:
theme_data = json.load(file)
# Get manifest details
manifest = theme_data.get("manifest", {})
name = manifest.get("name", "Unnamed")
description = manifest.get("description", "No description available")
author = manifest.get("author", "Unknown")
# Create display text and list item
display_text = f"{name}\n{description}\nBy: {author}"
list_item = QListWidgetItem(display_text)
list_item.setData(Qt.UserRole, json_file) # Store the JSON filename as metadata
# Style the name in bold
font = QFont()
font.setBold(False)
list_item.setFont(font)
json_files_list_widget.addItem(list_item)
# Apply spacing and styling to the list
json_files_list_widget.setStyleSheet("""
QListWidget {
padding: 1px;
}
QListWidget::item {
margin: 3px 0;
padding: 3px;
}
""")
def on_theme_selected(self, json_files_list_widget, current_theme_label):
selected_item = json_files_list_widget.currentItem()
if selected_item:
self.selected_theme = selected_item.data(Qt.UserRole)
current_theme_label.setText(f"Current Theme: {self.selected_theme}")
## REPOSITORY BLOCK BEGGINS
def download_themes_window(self):
dialog = QDialog(self)
dialog.setWindowTitle("Themes Repository")
dialog.setGeometry(100, 100, 800, 600)
main_layout = QHBoxLayout(dialog)
self.theme_list = QListWidget(dialog)
self.theme_list.setSelectionMode(QListWidget.SingleSelection)
self.theme_list.clicked.connect(self.on_theme_click)
main_layout.addWidget(self.theme_list)
right_layout = QVBoxLayout()
self.details_label = QLabel(dialog)
self.details_label.setWordWrap(True)
self.details_label.setStyleSheet("padding: 10px;")
right_layout.addWidget(self.details_label)
self.image_label = QLabel(dialog)
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setStyleSheet("padding: 10px;")
right_layout.addWidget(self.image_label)
download_button = QPushButton("Download Theme", dialog)
download_button.clicked.connect(self.theme_download)
right_layout.addWidget(download_button)
# Add a spacer to push the button to the bottom
spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
right_layout.addItem(spacer)
main_layout.addLayout(right_layout)
dialog.setLayout(main_layout)
self.load_themes()
dialog.exec_()
def fetch_themes(self):
try:
with open("config.json", "r") as config_file:
config = json.load(config_file)
url = config.get("ThemeRepository")
if not url:
raise ValueError("ThemeRepository is not defined in config.json")
response = requests.get(url)
response.raise_for_status()
return response.json()
except (FileNotFoundError, json.JSONDecodeError) as config_error:
self.show_error_popup("Error reading configuration", f"An error occurred while reading config.json: {config_error}")
return {}
except requests.exceptions.RequestException as fetch_error:
self.show_error_popup("Error fetching themes", f"An error occurred while fetching themes: {fetch_error}")
return {}
except ValueError as value_error:
self.show_error_popup("Configuration Error", str(value_error))
return {}
def download_theme_json(self, theme_url, theme_name):
try:
response = requests.get(theme_url)
response.raise_for_status()
if not os.path.exists('themes'):
os.makedirs('themes')
theme_filename = os.path.join('themes', f'{theme_name}.json')
with open(theme_filename, 'wb') as f:
f.write(response.content)
print(f"Downloaded {theme_name} theme to {theme_filename}")
except requests.exceptions.RequestException as e:
self.show_error_popup("Error downloading theme", f"An error occurred while downloading {theme_name}: {e}")
def show_error_popup(self, title, message):
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setWindowTitle(title)
msg.setText(message)
msg.exec_()
def is_theme_installed(self, theme_name):
return os.path.exists(os.path.join('themes', f'{theme_name}.json'))
def load_themes(self):
themes_data = self.fetch_themes()
themes = themes_data.get("themes", [])
installed_themes = []
uninstalled_themes = []
for theme in themes:
theme_display_name = f"{theme['name']} by {theme['author']}"
if self.is_theme_installed(theme['name']):
theme_display_name += " [I]"
installed_themes.append(theme_display_name)
else:
uninstalled_themes.append(theme_display_name)
self.theme_list.clear()
self.theme_list.addItems(uninstalled_themes)
self.theme_list.addItems(installed_themes)
# Autoselect the first item in the list if it exists
if self.theme_list.count() > 0:
self.theme_list.setCurrentRow(0)
self.on_theme_click()
def on_theme_click(self):
selected_item = self.theme_list.currentItem()
if selected_item:
theme_name = selected_item.text().split(" by ")[0]
theme = self.find_theme_by_name(theme_name)
if theme:
self.details_label.setText(
f"<b>Name:</b> {theme['name']}<br>"
f"<b>Description:</b> {theme['description']}<br>"
f"<b>Author:</b> {theme['author']}<br>"
f"<b>License:</b> {theme['license']}<br>"
f"<b>Link:</b> <a href='{theme['link']}'>{theme['link']}</a><br>"
)
self.details_label.setTextFormat(Qt.RichText)
self.details_label.setOpenExternalLinks(True)
preview = theme.get('preview')
if preview:
image_data = self.fetch_image(preview)
if image_data:
pixmap = QPixmap()
pixmap.loadFromData(image_data)
self.image_label.setPixmap(pixmap)
else:
self.image_label.clear()
def fetch_image(self, url):
try:
response = requests.get(url)
response.raise_for_status()
return response.content
except requests.exceptions.RequestException as e:
self.show_error_popup("Error fetching image", f"An error occurred while fetching the image: {e}")
return None
def find_theme_by_name(self, theme_name):
themes_data = self.fetch_themes()
themes = themes_data.get("themes", [])
for theme in themes:
if theme["name"] == theme_name:
return theme
return None
def theme_download(self):
selected_item = self.theme_list.currentItem()
if selected_item:
theme_name = selected_item.text().split(" by ")[0]
theme = self.find_theme_by_name(theme_name)
if theme:
theme_url = theme["link"]
self.download_theme_json(theme_url, theme_name)
self.load_themes()
## REPOSITORY BLOCK ENDS
def save_settings(self, is_rcp_enabled, check_updates_on_start, theme_background, selected_theme, is_bleeding):
config_path = "config.json"
updated_config = {
"IsRCPenabled": is_rcp_enabled,
"CheckUpdate": check_updates_on_start,
"ThemeBackground": theme_background,
"Theme": selected_theme,
"IsBleeding": is_bleeding
}
# Update config values
self.config.update(updated_config)
# Save updated config to file
with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4)
QMessageBox.information(
self,
"Settings Saved",
"Settings saved successfully!\n\nTo apply the changes, please restart the launcher."
)
self.__init__()
def get_palette(self, palette_type):
"""Retrieve the corresponding palette based on the palette type."""
palettes = {
"Dark": self.create_dark_palette,
"Obsidian": self.create_obsidian_palette,
"Redstone": self.create_redstone_palette,
"Alpha": self.create_alpha_palette,
"Strawberry": self.create_strawberry_palette,
"Native": self.create_native_palette,
"Christmas": self.create_christmas_palette,
}
# Default to dark palette if the type is not specified or invalid
return palettes.get(palette_type, self.create_dark_palette)()
def get_system_info(self):
# Get system information
java_version = subprocess.getoutput("java -version 2>&1 | head -n 1")
python_version = sys.version
pip_version = subprocess.getoutput("pip --version")
architecture = platform.architecture()[0]
operating_system = platform.system() + " " + platform.release()
# Get versions of installed pip packages
installed_packages = subprocess.getoutput("pip list")
return f"Java Version: {java_version}\nPython Version: {python_version}\nPip Version: {pip_version}\n" \
f"Architecture: {architecture}\nOperating System: {operating_system}\n\nPip Installed Packages:\n{installed_packages}"
def show_system_info(self):
system_info = self.get_system_info()
# Create a dialog to show the system info in a text box
info_dialog = QDialog(self)
info_dialog.setWindowTitle('Stats for Nerds')
layout = QVBoxLayout()
# Create a text box to display the system info
text_box = QTextEdit()
text_box.setText(system_info)
text_box.setReadOnly(True) # Make the text box read-only
layout.addWidget(text_box)
# Create a close button
close_button = QPushButton('Close')
close_button.clicked.connect(info_dialog.close)
layout.addWidget(close_button)
info_dialog.setLayout(layout)
info_dialog.exec_()
def open_game_directory(self):
try:
# Run the command and capture the output
result = subprocess.run(['picomc', 'instance', 'dir'], capture_output=True, text=True, check=True)
game_directory = result.stdout.strip()
# Open the directory in the system's file explorer
QDesktopServices.openUrl(QUrl.fromLocalFile(game_directory))
except subprocess.CalledProcessError as e:
print(f"Error running picomc command: {e}")
def populate_installed_versions(self):
config_path = "config.json"
# Check if the config file exists
if not os.path.exists(config_path):
logging.error("Config file not found.")
self.populate_installed_versions_normal_order()
return
# Load config from the file
try:
with open(config_path, "r") as config_file:
self.config = json.load(config_file)
except json.JSONDecodeError as e:
logging.error("Failed to load config: %s", e)
self.populate_installed_versions_normal_order()
return
# Run the command and capture the output
try:
process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, output=output, stderr=error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please ensure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error running 'picomc': %s", e.stderr)
return
# Parse the output and replace '[local]' with a space
versions = [version.replace('[local]', ' ').strip() for version in output.splitlines()]
# Get the last played version from the config
last_played = self.config.get("LastPlayed", "")
# If last played is not empty and is in the versions list, move it to the top
if last_played and last_played in versions:
versions.remove(last_played)
versions.insert(0, last_played)
# Populate the installed versions combo box
self.installed_version_combo.clear()
self.installed_version_combo.addItems(versions)
def populate_installed_versions_normal_order(self):
# Run the 'picomc instance create default' command at the start
try:
process = subprocess.Popen(['picomc', 'instance', 'create', 'default'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error creating default instance: %s", e.stderr)
return
# Run the 'picomc version list' command and get the output
try:
process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error: %s", e.stderr)
return
# Parse the output and replace '[local]' with a space
versions = output.splitlines()
versions = [version.replace('[local]', ' ').strip() for version in versions]
# Populate installed versions combo box
self.installed_version_combo.clear()
self.installed_version_combo.addItems(versions)
def open_marroc_script(self):
try:
# Use the interpreter from the current environment
interpreter = sys.executable
subprocess.Popen([interpreter, './marroc.py'])
except FileNotFoundError:
logging.error("'marroc.py' not found.")
QMessageBox.critical(self, "Error", "'marroc.py' not found.")
def play_instance(self):
if self.installed_version_combo.count() == 0:
QMessageBox.warning(self, "No Version Available", "Please download a version first.")
return
# Check if there are any accounts
try:
account_list_output = subprocess.check_output(["picomc", "account", "list"]).decode("utf-8").strip()
if not account_list_output:
QMessageBox.warning(self, "No Account Available", "Please create an account first.")
return
# Check if the selected account has a '*' (indicating it's the selected one)
if '*' not in account_list_output:
QMessageBox.warning(self, "No Account Selected", "Please select an account.")
return
except subprocess.CalledProcessError as e:
error_message = f"Error fetching accounts: {str(e)}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
return
selected_instance = self.installed_version_combo.currentText()
logging.info(f"Selected instance: {selected_instance}")
play_thread = threading.Thread(target=self.run_game, args=(selected_instance,))
play_thread.start()
def run_game(self, selected_instance):
try:
# Set current_state to the selected instance
self.current_state = selected_instance
# Read the config.json to get the "Instance" value
with open('config.json', 'r') as config_file:
config = json.load(config_file)
instance_value = config.get("Instance", "default") # Default to "default" if not found
# Update lastplayed field in config.json on a separate thread
update_thread = threading.Thread(target=self.update_last_played, args=(selected_instance,))
update_thread.start()
# Run the game subprocess with the instance_value from config.json
subprocess.run(['picomc', 'instance', 'launch', '--version-override', selected_instance, instance_value], check=True)
except subprocess.CalledProcessError as e:
error_message = f"Error playing {selected_instance}: {e}"
logging.error(error_message)
# Use QMetaObject.invokeMethod to call showError safely
QMetaObject.invokeMethod(
self, "showError", Qt.QueuedConnection,
Q_ARG(str, "Error"), Q_ARG(str, error_message)
)
finally:
# Reset current_state to "menu" after the game closes
self.current_state = "menu"
def update_last_played(self, selected_instance):
config_path = "config.json"
self.config["LastPlayed"] = selected_instance
with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4)
def showError(self, title, message):
QMessageBox.critical(self, title, message)
def manage_accounts(self):
# Main account management dialog
dialog = QDialog(self)
self.open_dialogs.append(dialog)
dialog.setWindowTitle('Manage Accounts')
dialog.setFixedSize(400, 250)
# Title
title_label = QLabel('Manage Accounts')
title_label.setFont(QFont("Arial", 14))
title_label.setAlignment(Qt.AlignCenter) # Center the text
# Dropdown for selecting accounts
account_combo = QComboBox()
self.populate_accounts(account_combo)
# Buttons
create_account_button = QPushButton('Create Account')
create_account_button.clicked.connect(self.open_create_account_dialog)
authenticate_button = QPushButton('Authenticate Account')
authenticate_button.clicked.connect(lambda: self.authenticate_account(dialog, account_combo.currentText()))
remove_account_button = QPushButton('Remove Account')
remove_account_button.clicked.connect(lambda: self.remove_account(dialog, account_combo.currentText()))
# New button to set the account idk
set_default_button = QPushButton('Select')
set_default_button.setFixedWidth(100) # Set button width to a quarter
set_default_button.clicked.connect(lambda: self.set_default_account(account_combo.currentText(), dialog))
# Layout for account selection (dropdown and set default button)
account_layout = QHBoxLayout()
account_layout.addWidget(account_combo)
account_layout.addWidget(set_default_button)
button_layout = QHBoxLayout()
button_layout.addWidget(create_account_button)
button_layout.addWidget(authenticate_button)
button_layout.addWidget(remove_account_button)
# Main layout
layout = QVBoxLayout()