-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgui.py
More file actions
1555 lines (1295 loc) · 60.8 KB
/
gui.py
File metadata and controls
1555 lines (1295 loc) · 60.8 KB
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 os
import threading
import json
import PyQt5
from PyQt5.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QTabWidget,
QLineEdit, QTextEdit, QFileDialog, QCheckBox, QHBoxLayout, QTimeEdit,
QSpinBox, QTableWidget, QTableWidgetItem, QComboBox,QListWidget, QMessageBox, QDialog
)
from PyQt5.QtCore import QTimer, Qt, QEvent
import auto_post # Import the auto-posting scheduler logic
import websockets
import asyncio
import schedule, time
import subprocess, sys # ✅ Needed to run scheduler_tester.py
from datetime import datetime
import platform
import pyautogui
from selenium import webdriver
from selenium.webdriver.common.by import By
import pickle
from auto_post import get_cookie_path # Add this import
from license_validator import LicenseValidator # Add this import
from logger_config import get_logger, setup_logging
SYSTEM = platform.system() # Will be 'Darwin' for Mac, 'Windows' for Windows
logger = get_logger('gui')
def get_app_config_path():
"""Gets the path for configuration files."""
if getattr(sys, 'frozen', False):
# Running as bundled app
app_support = os.path.join(
os.path.expanduser('~/Library/Application Support'),
'Facebook Scheduler'
)
else:
# Running in development
app_support = os.path.dirname(os.path.abspath(__file__))
# Create directory if it doesn't exist
os.makedirs(app_support, exist_ok=True)
return app_support
# Update CONFIG_FILE path
CONFIG_FILE = os.path.join(get_app_config_path(), "group_config.json")
def log_message(message):
"""Logs a message using the configured logger."""
try:
# Use the existing logger from logger_config
logger.info(message)
except Exception as e:
print(f"Error logging message: {str(e)}")
def get_next_post_time(group_name):
"""Fetches the next scheduled post time for a group from the scheduler."""
jobs = schedule.get_jobs()
# Filter jobs for this group and get their next run times
group_jobs = [job for job in jobs if group_name in str(job.job_func)
and job.job_func.__name__ != "reset_and_reschedule"] # Exclude reset job
if not group_jobs:
return "N/A"
# Get all future run times for this group's jobs
future_runs = []
current_time = datetime.now()
for job in group_jobs:
next_run = job.next_run
if next_run > current_time:
future_runs.append(next_run)
if not future_runs:
return "N/A"
# Return the earliest future run time
next_run = min(future_runs)
return next_run.strftime("%Y-%m-%d %H:%M:%S")
def is_group_running(group_name):
"""Checks if the group has an active schedule in both UI and scheduler."""
if group_name not in auto_post.running_schedules: # ✅ Ensure the group is being tracked
return False
jobs = schedule.get_jobs()
return any(group_name in str(job.job_func) for job in jobs) # ✅ Ensure the job actually exists
def get_scheduled_jobs():
"""Retrieves and saves all scheduled jobs to a shared file."""
jobs = schedule.get_jobs()
job_list = []
for job in jobs:
job_list.append({
"next_run": job.next_run.strftime("%Y-%m-%d %H:%M:%S"),
"job_func": str(job.job_func)
})
# Save to Application Support directory
jobs_file = os.path.join(get_app_config_path(), "scheduled_jobs.json")
with open(jobs_file, "w") as file:
json.dump(job_list, file, indent=4)
class LoginDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Facebook Login Required")
self.setMinimumWidth(400) # Make dialog wider for better readability
layout = QVBoxLayout()
# Add detailed instructions
instructions = QLabel(
"Important Instructions:\n\n"
"1. Make sure you have Chrome browser installed\n\n"
"2. When you click 'Login to Facebook':\n"
" • A Chrome browser will open\n"
" • Log in to your Facebook account\n"
" • Make sure to check 'Keep me logged in'\n"
" • After logging in, click OK on the next prompt\n\n"
"3. The browser will close automatically and save your login session\n\n"
"Note: You only need to do this once unless you log out\n"
"or change your Facebook password."
)
instructions.setWordWrap(True) # Enable text wrapping
layout.addWidget(instructions)
# Add spacer
layout.addSpacing(10)
# Add buttons
btn_layout = QHBoxLayout()
self.login_btn = QPushButton("Login to Facebook")
self.cancel_btn = QPushButton("Cancel")
btn_layout.addWidget(self.login_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
self.setLayout(layout)
# Connect buttons
self.login_btn.clicked.connect(self.accept)
self.cancel_btn.clicked.connect(self.reject)
class LicenseDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.validator = LicenseValidator()
self.setWindowTitle("License Check")
self.setMinimumWidth(400) # Make dialog wider for better readability
layout = QVBoxLayout()
# Add detailed instructions
instructions = QLabel(
"Important Instructions:\n\n"
"1. Make sure you have a valid license key\n\n"
"2. Enter your license key in the text box below\n\n"
"3. Click Activate License to proceed\n\n"
"Note: You need to enter a valid license key to use the app."
)
instructions.setWordWrap(True) # Enable text wrapping
layout.addWidget(instructions)
# Add spacer
layout.addSpacing(10)
# Add license key input
self.license_input = QTextEdit()
self.license_input.setPlaceholderText("Paste your license key here...")
self.license_input.setFixedHeight(80)
layout.addWidget(QLabel("License Key:"))
layout.addWidget(self.license_input)
# Status section
self.status_widget = QWidget()
status_layout = QVBoxLayout()
self.status_label = QLabel()
self.expiry_label = QLabel()
status_layout.addWidget(self.status_label)
status_layout.addWidget(self.expiry_label)
self.status_widget.setLayout(status_layout)
self.status_widget.hide()
layout.addWidget(self.status_widget)
# Add buttons
btn_layout = QHBoxLayout()
self.activate_btn = QPushButton("Activate License")
self.cancel_btn = QPushButton("Cancel")
# Style the activate button
self.activate_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border-radius: 5px;
font-weight: bold;
padding: 8px 16px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
btn_layout.addWidget(self.activate_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
self.setLayout(layout)
# Connect buttons
self.activate_btn.clicked.connect(self.validate_and_save)
self.cancel_btn.clicked.connect(self.reject)
def validate_and_save(self):
"""Validate and save the license key."""
license_key = self.license_input.toPlainText().strip()
if not license_key:
QMessageBox.warning(self, "Error", "Please enter a license key.")
return
is_valid, data = self.validator.validate_license(license_key)
if is_valid:
self.validator.save_license(license_key)
QMessageBox.information(
self,
"Success",
f"License activated successfully!\n\n"
f"User ID: {data['user_id']}\n"
f"Expiration: {data['expiration_date']}"
)
self.accept()
else:
QMessageBox.warning(
self,
"Error",
f"Invalid license key: {data.get('error', 'Unknown error')}\n\n"
"Please check your license key and try again."
)
class FacebookSchedulerApp(QWidget):
def __init__(self):
logger.info("Starting FacebookSchedulerApp initialization...")
logger.info("About to check license...")
super().__init__()
# Check license before proceeding
if not self.check_license():
logger.info("License check failed, exiting...")
sys.exit(1)
logger.info("License check passed, continuing initialization...")
# Add login status attribute
self.is_logged_in = False # Initialize as False
logger.info("Setting up UI components...")
self.groups = self.load_groups()
self.running_schedules = {}
self.failed_schedules = set()
# Load groups from file
self.groups = self.load_groups()
self.running_groups = {} # Track running groups
# Window properties
self.setWindowTitle('Facebook Auto Post Scheduler')
self.setGeometry(100, 100, 800, 600)
logger.info("Window properties set...")
# Main Layout
layout = QVBoxLayout()
# Create login controls layout
login_layout = QHBoxLayout()
# Add login status indicator
self.login_status_label = QLabel()
login_layout.addWidget(self.login_status_label)
# Add Check Login button
self.check_login_btn = QPushButton("Check Login Status")
self.check_login_btn.clicked.connect(self.verify_login_status)
login_layout.addWidget(self.check_login_btn)
# Add Delete Cookies button
self.delete_cookies_btn = QPushButton("Delete Login Session")
self.delete_cookies_btn.clicked.connect(self.delete_cookies)
login_layout.addWidget(self.delete_cookies_btn)
# Add login controls to main layout
layout.addLayout(login_layout)
# Update login status on startup
self.is_logged_in = self.check_login_status()
self.update_login_status()
# Group Selector Dropdown
self.group_selector = QComboBox(self)
self.group_selector.addItems(self.groups.keys())
self.group_selector.currentIndexChanged.connect(self.load_selected_group)
layout.addWidget(QLabel("Select Group:"))
layout.addWidget(self.group_selector)
# Tab Widget
self.tabs = QTabWidget()
layout.addWidget(self.tabs)
# Create Tabs
self.create_summary_tab()
self.create_schedule_tab()
self.create_groups_tab()
self.create_facebook_groups_tab()
self.create_texts_tab()
self.create_images_tab()
# Set Layout
self.setLayout(layout)
# Load Initial Group
self.load_selected_group()
# Start WebSocket Server in a Background Thread
self.start_websocket_server()
# Version check
check_for_updates()
# Analytics
collect_analytics()
def start_websocket_server(self):
"""Starts WebSocket server in a background thread."""
log_message("Starting WebSocket server...")
def run_server():
if SYSTEM == 'Windows':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(self.websocket_server())
except OSError as e:
if e.errno == 48: # Address already in use
log_message("WebSocket port 8765 is already in use. The application will continue without WebSocket support.")
else:
log_message(f"WebSocket server error: {str(e)}")
except Exception as e:
log_message(f"WebSocket server error: {str(e)}")
threading.Thread(target=run_server, daemon=True).start()
async def websocket_server(self):
"""Handles WebSocket communication from auto_post.py."""
try:
server = await websockets.serve(self.websocket_handler, "localhost", 8765)
await server.wait_closed()
except Exception as e:
log_message(f"WebSocket server setup error: {str(e)}")
raise
async def websocket_handler(self, websocket):
"""Handles WebSocket messages received from auto_post.py and refreshes the UI in the main thread."""
try:
async for message in websocket:
group_name = message.strip()
log_message(f"🔄 Received update for {group_name}, refreshing UI...")
# Use QTimer to safely update GUI from main thread
QTimer.singleShot(0, self.refresh_summary_table)
except websockets.exceptions.ConnectionClosedError:
log_message("⚠️ WebSocket connection closed unexpectedly.")
except Exception as e:
log_message(f"❌ WebSocket handler error: {e}")
def add_group(self):
"""Adds a new group."""
group_name = self.group_name_input.text().strip()
if not group_name:
self.show_error("Group name cannot be empty!")
return
if group_name in self.groups:
self.show_error("Group already exists!")
return
# Add group to dictionary
self.groups[group_name] = {
"group_urls": [],
"post_text": "",
"image_path": "",
"schedules": []
}
# Update UI
self.refresh_group_selector()
self.group_selector.setCurrentText(group_name) # Auto-select the new group
self.save_groups()
self.show_message(f"Group '{group_name}' added successfully!")
def delete_group(self):
"""Deletes the selected group and all associated data."""
group_name = self.group_selector.currentText()
if not group_name:
self.show_error("No group selected!")
return
# Confirm deletion
reply = QMessageBox.question(self, "Confirm Delete",
f"Are you sure you want to delete '{group_name}'?\n"
"This will remove all schedules, Facebook groups, images, and text for this group.",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
try:
# Stop any running schedules for this group
if group_name in auto_post.running_schedules:
self.stop_group(group_name)
# Remove group from dictionary
del self.groups[group_name]
# Update UI
self.refresh_group_selector()
self.facebook_group_list.clear() # Clear Facebook groups list
self.save_groups()
self.refresh_summary_table() # Refresh summary table
self.show_message(f"Group '{group_name}' deleted successfully!")
except Exception as e:
self.show_error(f"Error deleting group: {str(e)}")
def refresh_group_selector(self):
"""Updates the group selector dropdown."""
current_text = self.group_selector.currentText()
self.group_selector.clear()
self.group_selector.addItems(self.groups.keys())
# Try to restore previous selection if it still exists
index = self.group_selector.findText(current_text)
if index >= 0:
self.group_selector.setCurrentIndex(index)
elif self.group_selector.count() > 0:
self.group_selector.setCurrentIndex(0)
def create_groups_tab(self):
"""Creates the Groups Management Tab."""
self.groups_tab = QWidget()
layout = QVBoxLayout()
# Input for Group Name
self.group_name_input = QLineEdit(self)
layout.addWidget(QLabel("Group Name:"))
layout.addWidget(self.group_name_input)
# Buttons to Add/Delete Groups
btn_layout = QHBoxLayout()
self.add_group_btn = QPushButton("Add Group")
self.delete_group_btn = QPushButton("Delete Group")
self.add_group_btn.clicked.connect(self.add_group)
self.delete_group_btn.clicked.connect(self.delete_group)
btn_layout.addWidget(self.add_group_btn)
btn_layout.addWidget(self.delete_group_btn)
layout.addLayout(btn_layout)
self.groups_tab.setLayout(layout)
self.tabs.addTab(self.groups_tab, "Manage Groups")
def create_summary_tab(self):
"""Creates the Summary Tab with Group Status Table."""
self.summary_tab = QWidget()
layout = QVBoxLayout()
# Group Summary Table
self.summary_table = QTableWidget()
self.summary_table.setColumnCount(4)
self.summary_table.setHorizontalHeaderLabels(["Group Name", "Status", "Next Post", "Actions"])
# Make summary table read-only
self.summary_table.setEditTriggers(QTableWidget.NoEditTriggers)
layout.addWidget(self.summary_table)
# Start/Stop All Buttons
btn_layout = QHBoxLayout()
self.start_all_btn = QPushButton("Start All")
self.stop_all_btn = QPushButton("Stop All")
self.fast_forward_btn = QPushButton("Fast-Forward ⏩") # ✅ Added fast-forward button
self.start_all_btn.clicked.connect(self.start_all_groups)
self.stop_all_btn.clicked.connect(self.stop_all_groups)
self.fast_forward_btn.clicked.connect(self.fast_forward_scheduler) # ✅ Connect button to function
btn_layout.addWidget(self.start_all_btn)
btn_layout.addWidget(self.stop_all_btn)
btn_layout.addWidget(self.fast_forward_btn) # ✅ Add fast-forward button to UI
layout.addLayout(btn_layout)
self.summary_tab.setLayout(layout)
self.tabs.addTab(self.summary_tab, "Summary")
# Refresh Table on Startup
self.refresh_summary_table()
def fast_forward_scheduler(self):
"""Triggers fast-forwarding of the scheduler from the GUI."""
try:
# Run the fast forward in a separate thread
def run_fast_forward():
result = auto_post.fast_forward_scheduler()
# Use QTimer to safely update GUI from main thread
QTimer.singleShot(0, lambda: self.handle_fast_forward_result(result))
threading.Thread(target=run_fast_forward, daemon=True).start()
except Exception as e:
log_message(f"❌ Error in fast forward: {str(e)}")
self.show_message(f"Fast forward failed: {str(e)}")
def handle_fast_forward_result(self, result):
"""Handles the result of fast forward operation on the main thread."""
self.show_message(result)
self.refresh_summary_table()
def refresh_summary_table(self):
"""Queues a request to update the summary table safely in the main thread."""
if threading.current_thread().name != "MainThread":
# If called from non-main thread, use QTimer to safely update
QTimer.singleShot(0, self.update_table)
else:
# If already on main thread, update directly
self.update_table()
def start_all_groups(self):
"""Modified to include comprehensive logging."""
logger.info("Attempting to start all groups")
if not self.is_logged_in:
logger.warning("Login required before starting groups")
reply = QMessageBox.question(self, "Login Required",
"You need to login first. Would you like to login now?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
if self.handle_login():
self.is_logged_in = True
logger.info("Login successful")
else:
logger.error("Login failed")
return
else:
logger.info("Login cancelled by user")
return
def run_start_all():
logger.info("Starting all groups process")
# Clear all existing schedules first
schedule.clear()
logger.debug("Schedule cleared")
auto_post.running_schedules.clear()
logger.info("Cleared all existing schedules")
# Start all groups and track success
for group_name in self.groups:
try:
logger.info(f"Starting scheduling for group: {group_name}")
if auto_post.schedule_group_posts(group_name, self.groups[group_name]):
auto_post.running_schedules[group_name] = True
logger.info(f"Successfully scheduled group: {group_name}")
else:
logger.error(f"Failed to schedule group: {group_name}")
except Exception as e:
logger.error(f"Error scheduling group {group_name}: {str(e)}")
if auto_post.running_schedules:
auto_post.start_scheduler()
# Update UI
QTimer.singleShot(0, self.refresh_summary_table)
logger.info("Scheduled UI refresh")
# Run in background thread
threading.Thread(target=run_start_all, daemon=True).start()
logger.info("Started scheduling thread")
def emit_start_result(self, group_name, success):
"""Safely emit start result to main thread."""
QApplication.instance().postEvent(
self,
QStartResultEvent(group_name, success)
)
def emit_refresh_table(self):
"""Safely emit refresh table event to main thread."""
QApplication.instance().postEvent(
self,
QRefreshTableEvent()
)
def stop_all_groups(self):
"""Stops scheduling for all groups."""
log_message("Stopping all group schedules")
# Clear all scheduled jobs
schedule.clear()
# Clear running schedules tracking
auto_post.running_schedules.clear()
# Clear failed schedules
self.failed_schedules.clear()
log_message("All group schedules stopped")
self.refresh_summary_table()
def start_group(self, group_name):
"""Starts scheduling for an individual group."""
if not self.is_logged_in:
reply = QMessageBox.question(self, "Login Required",
"You need to login first. Would you like to login now?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
if self.handle_login():
self.is_logged_in = True
else:
return
else:
return
def run_start():
try:
# Get current jobs for this group before starting
existing_jobs = [job for job in schedule.get_jobs()
if job_func_matches_group(job, group_name)]
# Only clear this group's jobs
for job in existing_jobs:
schedule.cancel_job(job)
log_message(f"Cleared existing schedules for {group_name}")
# Schedule the group's posts
if auto_post.schedule_group_posts(group_name, self.groups[group_name]):
log_message(f"++Successfully scheduled posts for {group_name}")
#QTimer.singleShot(0, lambda: self.handle_start_result(group_name, True))
self.handle_start_result(group_name, True)
else:
log_message(f"Failed to schedule posts for {group_name}")
#QTimer.singleShot(0, lambda: self.handle_start_result(group_name, False))
self.handle_start_result(group_name, False)
except Exception as e:
log_message(f"Error starting group {group_name}: {str(e)}")
#QTimer.singleShot(0, lambda: self.handle_start_result(group_name, False))
self.handle_start_result(group_name, False)
threading.Thread(target=run_start, daemon=True).start()
# Force refresh of summary table
QTimer.singleShot(100, self.refresh_summary_table)
def handle_start_result(self, group_name, success):
"""Handles the result of starting a group on the main thread."""
if success:
auto_post.running_schedules[group_name] = True
self.failed_schedules.discard(group_name)
# Only start scheduler if there are running schedules and it's not already running
if auto_post.running_schedules and (not auto_post.scheduler_thread or not auto_post.scheduler_thread.is_alive()):
auto_post.start_scheduler()
log_message(f"Successfully started scheduler for {group_name}")
elif group_name not in auto_post.running_schedules:
log_message(f"Error when scheduling tasks for {group_name}.")
self.failed_schedules.add(group_name)
else:
log_message(f"{group_name} already has a schedule running.")
# Refresh summary table once with a small delay to ensure all updates are captured
QTimer.singleShot(100, self.refresh_summary_table)
def stop_group(self, group_name):
"""Stops scheduling for an individual group."""
def run_stop():
if group_name in self.groups:
log_message(f"Stopping scheduling for group: {group_name}")
jobs_to_remove = [job for job in schedule.get_jobs()
if job_func_matches_group(job, group_name)]
for job in jobs_to_remove:
schedule.cancel_job(job)
log_message(f"Removed scheduled job for {group_name}")
if group_name in auto_post.running_schedules:
del auto_post.running_schedules[group_name]
log_message(f"Removed {group_name} from running schedules")
# Update UI safely from main thread
QTimer.singleShot(0, self.refresh_summary_table)
log_message(f"Successfully stopped group: {group_name}")
threading.Thread(target=run_stop, daemon=True).start()
def create_schedule_tab(self):
"""Creates the Schedule Management Tab."""
self.schedule_tab = QWidget()
layout = QVBoxLayout()
# Days Selection
self.day_checkboxes = {}
days_layout = QHBoxLayout()
for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]:
checkbox = QCheckBox(day, self)
self.day_checkboxes[day] = checkbox
days_layout.addWidget(checkbox)
layout.addLayout(days_layout)
# Time Selection
self.start_time_picker = QTimeEdit(self)
self.start_time_picker.setDisplayFormat("HH:mm")
self.end_time_picker = QTimeEdit(self)
self.end_time_picker.setDisplayFormat("HH:mm")
time_layout = QHBoxLayout()
time_layout.addWidget(QLabel("Start Time:"))
time_layout.addWidget(self.start_time_picker)
time_layout.addWidget(QLabel("End Time:"))
time_layout.addWidget(self.end_time_picker)
layout.addLayout(time_layout)
# Number of Posts
self.num_posts_spinbox = QSpinBox(self)
self.num_posts_spinbox.setRange(1, 24) # Allow up to 24 posts (one per hour max)
layout.addWidget(QLabel("Number of Posts in Time Period:"))
layout.addWidget(self.num_posts_spinbox)
# Add Schedule Button
self.add_schedule_btn = QPushButton("Add Schedule")
self.add_schedule_btn.clicked.connect(self.add_schedule)
layout.addWidget(self.add_schedule_btn)
# Schedule Table
self.schedule_table = QTableWidget()
self.schedule_table.setColumnCount(3)
self.schedule_table.setHorizontalHeaderLabels(["Days", "Time Period", "Posts"])
# Make schedule table editable with specific triggers
self.schedule_table.setEditTriggers(
QTableWidget.DoubleClicked |
QTableWidget.EditKeyPressed |
QTableWidget.AnyKeyPressed
)
self.schedule_table.cellChanged.connect(self.handle_cell_edit)
layout.addWidget(self.schedule_table)
self.schedule_tab.setLayout(layout)
self.tabs.addTab(self.schedule_tab, "Manage Schedules")
# **New Buttons for Deleting and Updating**
btn_layout = QHBoxLayout()
self.delete_schedule_btn = QPushButton("Delete Selected Schedule")
self.delete_schedule_btn.clicked.connect(self.delete_schedule)
self.update_schedule_btn = QPushButton("Update Selected Schedule")
self.update_schedule_btn.clicked.connect(self.update_schedule)
btn_layout.addWidget(self.delete_schedule_btn)
btn_layout.addWidget(self.update_schedule_btn)
layout.addLayout(btn_layout)
self.schedule_tab.setLayout(layout)
self.tabs.addTab(self.schedule_tab, "Manage Schedules")
def delete_schedule(self):
"""Deletes the selected schedule row."""
selected_row = self.schedule_table.currentRow()
if selected_row >= 0:
group_name = self.group_selector.currentText()
if group_name in self.groups and "schedules" in self.groups[group_name]:
del self.groups[group_name]["schedules"][selected_row] # ✅ Delete from data
self.schedule_table.removeRow(selected_row) # ✅ Remove from UI table
self.save_groups() # ✅ Save changes
self.show_message("❌ Schedule deleted successfully.")
else:
self.show_error("⚠️ Please select a schedule to delete!")
def update_schedule(self):
"""Updates the selected schedule with new values."""
selected_items = self.schedule_table.selectedItems()
if not selected_items:
self.show_error("Please select a schedule to update!")
return
# Get current group
group_name = self.group_selector.currentText()
if not group_name in self.groups:
self.show_error("No group selected!")
return
# Get selected row
row = selected_items[0].row()
try:
# Parse values directly from the table cells
days_str = self.schedule_table.item(row, 0).text()
time_period = self.schedule_table.item(row, 1).text()
num_posts = int(self.schedule_table.item(row, 2).text())
# Parse days
selected_days = [day.strip() for day in days_str.split(",")]
# Parse time period
start_time, end_time = time_period.split(" - ")
# Update the schedule in the group data
schedule_index = row
if schedule_index < len(self.groups[group_name]["schedules"]):
self.groups[group_name]["schedules"][schedule_index] = {
"days": selected_days,
"start_time": start_time,
"end_time": end_time,
"num_posts": num_posts
}
# Save changes
self.save_groups()
# If group is currently running, restart it to apply new schedule
if group_name in auto_post.running_schedules:
self.stop_group(group_name)
self.start_group(group_name)
self.show_message("✅ Schedule updated successfully!")
else:
self.show_error("Invalid schedule index!")
except Exception as e:
self.show_error(f"Error updating schedule: {str(e)}")
def add_schedule(self):
"""Adds a schedule entry for the selected group."""
selected_days = [day for day, checkbox in self.day_checkboxes.items() if checkbox.isChecked()]
if not selected_days:
self.show_error("Please select at least one day!")
return
start_time = self.start_time_picker.time().toString("HH:mm")
end_time = self.end_time_picker.time().toString("HH:mm")
num_posts = self.num_posts_spinbox.value()
group_name = self.group_selector.currentText()
if group_name not in self.groups:
self.show_error("Invalid group selection.")
return
schedule_entry = {
"days": selected_days,
"start_time": start_time,
"end_time": end_time,
"num_posts": num_posts
}
# Add schedule to selected group
if "schedules" not in self.groups[group_name]:
self.groups[group_name]["schedules"] = []
self.groups[group_name]["schedules"].append(schedule_entry)
# Refresh Table with the updated schedules
self.refresh_schedule_table()
self.save_groups()
self.refresh_summary_table()
def refresh_schedule_table(self):
"""Refreshes the schedule table display."""
self.schedule_table.setRowCount(0)
group_name = self.group_selector.currentText()
if group_name in self.groups and "schedules" in self.groups[group_name]:
for schedule in self.groups[group_name]["schedules"]:
row = self.schedule_table.rowCount()
self.schedule_table.insertRow(row)
days_str = ", ".join(schedule["days"])
time_str = f"{schedule['start_time']} - {schedule['end_time']}"
posts_str = str(schedule["num_posts"])
self.schedule_table.setItem(row, 0, QTableWidgetItem(days_str))
self.schedule_table.setItem(row, 1, QTableWidgetItem(time_str))
self.schedule_table.setItem(row, 2, QTableWidgetItem(posts_str))
def save_groups(self):
"""Saves the groups to the configuration file."""
try:
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
with open(CONFIG_FILE, "w", encoding='utf-8') as file:
json.dump({"groups": self.groups}, file, ensure_ascii=False, indent=2)
except Exception as e:
self.show_error(f"Failed to save groups: {str(e)}")
def show_error(self, message):
"""Displays and logs an error message."""
logger.error(message)
QMessageBox.critical(self, "Error", message)
def show_confirm(self, message):
"""Displays and logs a confirmation dialog."""
logger.info(f"Confirmation requested: {message}")
reply = QMessageBox.question(self, "Confirm", message,
QMessageBox.Yes | QMessageBox.No)
return reply == QMessageBox.Yes
def show_message(self, message):
"""Displays and logs a success message."""
logger.info(message)
QMessageBox.information(self, "Success", message)
def load_groups(self):
"""Loads groups from a config file."""
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding='utf-8') as file:
return json.load(file).get("groups", {})
except Exception as e:
self.show_error(f"Failed to load groups: {str(e)}")
log_message(f"Error loading groups: {str(e)}")
return {}
def load_selected_group(self):
"""Loads the selected group's data into the UI."""
group_name = self.group_selector.currentText()
if not group_name:
return
if group_name in self.groups:
group_data = self.groups[group_name]
# Load profile type
profile_type = group_data.get("profile_type", "")
self.personal_profile_cb.setChecked(profile_type == "personal")
self.business_profile_cb.setChecked(profile_type == "business")
# Load other group data...
self.facebook_group_list.clear()
self.facebook_group_list.addItems(group_data.get("group_urls", []))
self.post_text_input.setPlainText(group_data.get("post_text", ""))
self.image_path_input.setText(group_data.get("image_path", ""))
self.refresh_schedule_table()
def browse_image(self):
"""Opens a file dialog to select an image file."""
options = QFileDialog.Options()
if SYSTEM == 'Windows':
options |= QFileDialog.DontUseNativeDialog
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Image",
os.path.expanduser("~"), # Start in user's home directory
"Images (*.png *.jpg *.jpeg)",
options=options
)
if file_path:
try:
# Use the original file path
file_path = os.path.normpath(file_path)
# Update the image path in the UI and group data
self.image_path_input.setText(file_path)
# Save the path in group data
group_name = self.group_selector.currentText()
if group_name in self.groups:
self.groups[group_name]["image_path"] = file_path
self.save_groups()
self.show_message(f"✅ Image path saved: {file_path}")
except Exception as e:
self.show_error(f"Error saving image path: {str(e)}")
return
def create_texts_tab(self):
"""Creates the Post Text Management Tab."""
self.texts_tab = QWidget()
layout = QVBoxLayout()
# Post Text Input
self.post_text_input = QTextEdit(self)
layout.addWidget(QLabel("Post Text:"))
layout.addWidget(self.post_text_input)
# Save Text Button
self.save_text_btn = QPushButton("Save Text")
self.save_text_btn.clicked.connect(self.save_text)
layout.addWidget(self.save_text_btn)
self.texts_tab.setLayout(layout)
self.tabs.addTab(self.texts_tab, "Manage Texts")
def create_images_tab(self):
"""Creates the Image Management Tab."""
self.images_tab = QWidget()
layout = QVBoxLayout()
# Image File Path Input
self.image_path_input = QLineEdit(self)
layout.addWidget(QLabel("Image File:"))
layout.addWidget(self.image_path_input)
# Browse Button
self.browse_image_btn = QPushButton("Browse")
self.browse_image_btn.clicked.connect(self.browse_image)
layout.addWidget(self.browse_image_btn)