From 671fadf645ec2465530bfb42fc431bf945e00415 Mon Sep 17 00:00:00 2001 From: nkhangg Date: Thu, 16 Oct 2025 21:56:59 +0700 Subject: [PATCH] upgrate PyQt6 --- app.py | 6 +- facebook_marketplace.db | Bin 49152 -> 49152 bytes fb_window.py | 18 +- gui/core/login_handle_dialog.py | 25 +- gui/global_signals.py | 9 +- gui/handle/login_fb.py | 72 ++++-- gui/main_window.py | 49 ++-- gui/tabs/accounts/account_tab.py | 82 ++++--- gui/tabs/accounts/forms/account_form.py | 39 ++- gui/tabs/import_tab.py | 34 +-- .../listeds/dialogs/listed_filter_dialog.py | 22 +- gui/tabs/listeds/listed_tab.py | 228 ++++++++++++------ .../products/dialogs/add_listed_dialog.py | 22 +- .../products/dialogs/product_filter_dialog.py | 22 +- gui/tabs/products/forms/product_form.py | 85 +++++-- gui/tabs/products/product_tab.py | 158 +++++++----- gui/tabs/settings/forms/setting_form.py | 41 +++- gui/tabs/settings/settings_tab.py | 73 ++++-- requirements.txt | 5 +- services/action_service.py | 4 +- services/core/loading_service.py | 13 +- services/image_service.py | 13 +- services/profile_service.py | 25 +- stores/shared_store.py | 5 +- 24 files changed, 703 insertions(+), 347 deletions(-) diff --git a/app.py b/app.py index 6c8374a..b581465 100644 --- a/app.py +++ b/app.py @@ -1,14 +1,16 @@ import sys -from PyQt5.QtWidgets import QApplication +from PyQt6.QtWidgets import QApplication from gui.main_window import MainWindow from database.db import create_tables + def main(): create_tables() # tạo bảng nếu chưa tồn tại app = QApplication(sys.argv) window = MainWindow() window.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) + if __name__ == "__main__": main() diff --git a/facebook_marketplace.db b/facebook_marketplace.db index 2863f8d8ab30b071eab7a45829398848e6edd5b4..3742794ddb776c271c5d4434eca62a2a820e72c9 100644 GIT binary patch delta 549 zcmZwCJxjwt9LMqdCzt2ev{n?2ks2*JXdkQyDpIgc3OZCLky6{x>W~%_>DYj7B3L_i zaB^@{&N@j~S6_s);2TiyCJmH>-F?@yKE+>{pLc@r9E z^~!nKb4S9!o1ANV-icFn(KGqTnQ>EJMW82;o)~mUG$QG(VNFB|U_+~YNxKc(snG8c zLT}8}+UK|YXM~v&v^!>U(5{$CL2EJ-gI1>2GK3)Ru@`aB)|v4^n`6cWEv6{g_(!V# zO7Gu8dT-F7w?v4HMsD?O5i0PW_2gu6t2Y~$#~Y&e2a5*oV{usJhs7l_}FLq~s*JL>(~aweNR uv^;AeZk5SAf3)+^Cc&~U#pYNmqnTWAKJw2U&Ak-?t+*A delta 433 zcmYk%KT88K7{~Gac`la(+ovsV@x(WvxOOVEv(UGoUQw58@vd_ZoE(Jq1qg0BdITIw zaS<0cH=SI>O(!=&J(4BU7e3@6{K}G-C7&BQTx<-YA=$~6ReI3d)N7n4baFaQr-wzp z-k>~T`KTyPvZI{b*!NztWe6b}y97V+6(6y}OFYLZ?&BVIZM&7=tfK`{6pTLp`jwMW zme$uD#%Az4im& z@CBdo2_JA(FTKKn07a?RE=svb=@Q2-Lbybc3xEsfE{p*vW&dVuSJgjB(I?HZ)wc_% T=GWBr@3tkJH5)CP-(S#wz2|E6 diff --git a/fb_window.py b/fb_window.py index ad19b16..90c2891 100644 --- a/fb_window.py +++ b/fb_window.py @@ -2,8 +2,8 @@ import os import sys import cv2 import numpy as np -from PyQt5.QtCore import Qt, QUrl, QTimer -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import Qt, QUrl, QTimer +from PyQt6.QtWidgets import ( QApplication, QMainWindow, QVBoxLayout, @@ -12,8 +12,8 @@ from PyQt5.QtWidgets import ( QLabel, QDialog, ) -from PyQt5.QtWebEngineWidgets import QWebEngineView -from PyQt5.QtGui import QPixmap, QImage +from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtGui import QPixmap, QImage from services.action_service import ActionService # ✅ JS version (fake click/type) @@ -37,7 +37,7 @@ class FBWindow(QMainWindow): self.btn_detect.clicked.connect(self.detect_inputs_from_view) self.status = QLabel("Status: Ready") - self.status.setAlignment(Qt.AlignLeft) + self.status.setAlignment(Qt.AlignmentFlag.AlignLeft) layout = QVBoxLayout() layout.addWidget(self.web) @@ -195,7 +195,7 @@ class FBWindow(QMainWindow): # ---------------------------------------------------- def show_preview(self, bgr_img): - """Hiển thị preview trong PyQt5""" + """Hiển thị preview trong PyQt6""" rgb = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB) h, w = rgb.shape[:2] qimg = QImage(rgb.data, w, h, rgb.strides[0], QImage.Format.Format_RGB888) @@ -205,13 +205,13 @@ class FBWindow(QMainWindow): dlg.setWindowTitle("Detection Preview") v_layout = QVBoxLayout(dlg) lbl = QLabel() - lbl.setPixmap(pix.scaled(800, 600, Qt.KeepAspectRatio)) + lbl.setPixmap(pix.scaled(800, 600, Qt.AspectRatioMode.KeepAspectRatio)) v_layout.addWidget(lbl) - dlg.exec_() + dlg.exec() if __name__ == "__main__": app = QApplication(sys.argv) win = FBWindow() win.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) diff --git a/gui/core/login_handle_dialog.py b/gui/core/login_handle_dialog.py index e0eb26d..3488eb7 100644 --- a/gui/core/login_handle_dialog.py +++ b/gui/core/login_handle_dialog.py @@ -1,6 +1,6 @@ # gui/core/login_handle_dialog.py -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QPushButton, QApplication -from PyQt5.QtCore import QTimer +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QApplication +from PyQt6.QtCore import QTimer from services.core.log_service import log_service from stores.shared_store import SharedStore from gui.global_signals import global_signals @@ -11,7 +11,7 @@ class LoginHandleDialog(QDialog): dialog_height = 100 margin = 10 # khoảng cách giữa các dialog - # Lưu danh sách dialog đang mở (dùng class variable để chia sẻ giữa tất cả dialog) + # Lưu danh sách dialog đang mở (chia sẻ giữa tất cả dialog) open_dialogs = [] def __init__(self, account_id: int, listed_id: int): @@ -30,11 +30,12 @@ class LoginHandleDialog(QDialog): layout.addWidget(self.btn_finish) self.setLayout(layout) - # --- Tính vị trí để xếp dialog từ góc trái trên cùng sang phải theo hàng ngang --- + # --- Vị trí dialog --- self.move_to_corner() LoginHandleDialog.open_dialogs.append(self) def move_to_corner(self): + """Tính vị trí để xếp dialog từ góc trái trên cùng sang phải theo hàng ngang""" screen_geometry = QApplication.primaryScreen().availableGeometry() start_x = screen_geometry.left() + self.margin start_y = screen_geometry.top() + self.margin @@ -66,15 +67,25 @@ class LoginHandleDialog(QDialog): try: store = SharedStore.get_instance() store.remove(self.listed_id) - log_service.info(f"[Dialog] Removed listed_id={self.listed_id} from SharedStore") + log_service.info( + f"[Dialog] Removed listed_id={self.listed_id} from SharedStore" + ) # Emit signal để MainWindow biết - QTimer.singleShot(0, lambda: global_signals.dialog_finished.emit(self.account_id, self.listed_id)) + QTimer.singleShot( + 0, + lambda: global_signals.dialog_finished.emit( + self.account_id, self.listed_id + ), + ) # Close dialog an toàn self.hide() self.deleteLater() if self in LoginHandleDialog.open_dialogs: LoginHandleDialog.open_dialogs.remove(self) + except Exception as e: - log_service.error(f"[Dialog] Exception in finish_listing for listed_id={self.listed_id}: {e}") + log_service.error( + f"[Dialog] Exception in finish_listing for listed_id={self.listed_id}: {e}" + ) diff --git a/gui/global_signals.py b/gui/global_signals.py index 68ace94..a115125 100644 --- a/gui/global_signals.py +++ b/gui/global_signals.py @@ -1,9 +1,12 @@ # gui/global_signals.py -from PyQt5.QtCore import QObject, pyqtSignal +from PyQt6.QtCore import QObject, pyqtSignal + class GlobalSignals(QObject): listed_finished = pyqtSignal() open_login_dialog = pyqtSignal(int, int) # account_id, listed_id - dialog_finished = pyqtSignal(int, int) # account_id, listed_id + dialog_finished = pyqtSignal(int, int) # account_id, listed_id -global_signals = GlobalSignals() \ No newline at end of file + +# Tạo instance toàn cục để các module khác có thể import dùng chung +global_signals = GlobalSignals() diff --git a/gui/handle/login_fb.py b/gui/handle/login_fb.py index cf2e40e..ede3e95 100644 --- a/gui/handle/login_fb.py +++ b/gui/handle/login_fb.py @@ -2,10 +2,19 @@ import os import sys import cv2 import numpy as np -from PyQt5.QtCore import Qt, QUrl, QTimer -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QTextEdit, QPushButton -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile -from PyQt5.QtGui import QImage +from PyQt6.QtCore import Qt, QUrl, QTimer +from PyQt6.QtWidgets import ( + QApplication, + QMainWindow, + QVBoxLayout, + QWidget, + QLabel, + QTextEdit, + QPushButton, +) +from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtWebEngineCore import QWebEngineProfile +from PyQt6.QtGui import QImage from services.action_service import ActionService from services.detect_service import DetectService @@ -14,41 +23,47 @@ from config import TEMPLATE_DIR class LoginFB(QMainWindow): + """Cửa sổ tự động đăng nhập Facebook bằng nhận diện hình ảnh.""" + def __init__(self, account=None, delay=0.3): super().__init__() self.account = account or {} - self.template_dir = os.path.abspath(TEMPLATE_DIR) self.delay = delay + self.template_dir = os.path.abspath(TEMPLATE_DIR) # ✅ Lấy tên profile từ email hoặc username - self.profile_name = self.account.get("email") or self.account.get("username") or "default" + self.profile_name = ( + self.account.get("email") or self.account.get("username") or "default" + ) # --- Detect services --- self.detector = DetectService( - template_dir=TEMPLATE_DIR, - target_labels=["username", "password"] + template_dir=TEMPLATE_DIR, target_labels=["username", "password"] ) - # --- UI cơ bản --- + # --- UI setup --- self.setWindowTitle(f"FB Auto Vision Login - {self.profile_name}") self.setFixedSize(480, 680) self.web = QWebEngineView() - self.status = QLabel("Status: Ready") - self.status.setAlignment(Qt.AlignLeft) + self.status.setAlignment(Qt.AlignmentFlag.AlignLeft) self.status.setFixedHeight(20) + # Log area self.log_area = QTextEdit() self.log_area.setReadOnly(True) self.log_area.setFixedHeight(120) - self.log_area.setStyleSheet(""" + self.log_area.setStyleSheet( + """ background-color: #1e1e1e; color: #dcdcdc; font-size: 12px; font-family: Consolas, monospace; - """) + """ + ) + # Refresh button self.btn_refresh = QPushButton("Refresh") self.btn_refresh.setFixedHeight(30) self.btn_refresh.clicked.connect(self.refresh_page) @@ -56,9 +71,14 @@ class LoginFB(QMainWindow): # --- Profile --- self.profile_service = ProfileService() profile_path = self.profile_service.get_profile_path(self.profile_name) + os.makedirs(profile_path, exist_ok=True) + profile = self.web.page().profile() - profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies) + profile.setPersistentCookiesPolicy( + QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies + ) profile.setPersistentStoragePath(profile_path) + self.log(f"[INFO] Profile applied at: {profile_path}") # --- Webview --- @@ -83,15 +103,18 @@ class LoginFB(QMainWindow): # ---------------------------------------------------- def log(self, message: str): + """Ghi log vào vùng log và console.""" self.log_area.append(message) print(message) # ---------------------------------------------------- def capture_webview(self): + """Chụp hình ảnh nội dung webview dưới dạng numpy array (BGR).""" pixmap = self.web.grab() if pixmap.isNull(): return None - qimg = pixmap.toImage().convertToFormat(QImage.Format_RGBA8888) + + qimg = pixmap.toImage().convertToFormat(QImage.Format.Format_RGBA8888) width, height = qimg.width(), qimg.height() ptr = qimg.bits() ptr.setsize(height * width * 4) @@ -100,12 +123,13 @@ class LoginFB(QMainWindow): # ---------------------------------------------------- def on_web_loaded(self, ok=True): + """Khi trang web load xong.""" if not ok: self.log("[ERROR] Page failed to load") self.status.setText("Status: Page load failed") return - self.log("[INFO] Page loaded") + self.log("[INFO] Page loaded successfully") self.status.setText("Status: Page loaded") # ✅ Lưu profile khi load xong @@ -116,7 +140,7 @@ class LoginFB(QMainWindow): screen = self.capture_webview() if screen is None: self.status.setText("Status: Unable to capture webview") - self.log("Status: Unable to capture webview") + self.log("[WARN] Unable to capture webview") return self.log("[INFO] Detecting email/password fields...") @@ -129,19 +153,25 @@ class LoginFB(QMainWindow): self.status.setText(f"[INFO] Detected {len(regions)} valid regions") self.log(f"[INFO] Detected {len(regions)} valid regions") + + # Chờ 500ms trước khi tự điền form QTimer.singleShot(500, lambda: self.autofill_by_detection(regions)) # ---------------------------------------------------- def autofill_by_detection(self, regions): + """Tự động điền email và password dựa vào vùng phát hiện.""" email = self.account.get("email", "") password = self.account.get("password", "") - # sắp xếp để điền username trước, password sau - ordered = sorted(regions, key=lambda r: ("pass" in r[0].lower(), "user" not in r[0].lower())) + # sắp xếp: username trước, password sau + ordered = sorted( + regions, key=lambda r: ("pass" in r[0].lower(), "user" not in r[0].lower()) + ) def do_action(i=0): if i >= len(ordered): return + folder_name, filename, top_left, bottom_right, score = ordered[i] label = folder_name.lower() @@ -161,13 +191,15 @@ class LoginFB(QMainWindow): # ---------------------------------------------------- def refresh_page(self): + """Tải lại trang.""" self.log("[INFO] Refreshing page...") self.web.reload() +# ---------------------------------------------------- if __name__ == "__main__": app = QApplication(sys.argv) fake_account = {"email": "test@example.com", "password": "123456"} win = LoginFB(account=fake_account, delay=0.5) win.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) diff --git a/gui/main_window.py b/gui/main_window.py index ac95712..4642b5c 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QMainWindow, QTabWidget, QMessageBox +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QMessageBox from gui.tabs.accounts.account_tab import AccountTab from gui.tabs.products.product_tab import ProductTab from gui.tabs.import_tab import ImportTab @@ -34,51 +34,62 @@ class MainWindow(QMainWindow): self.setCentralWidget(self.tabs) self.on_tab_changed(0) - # # --- Signals --- + # --- Signals --- # global_signals.listed_finished.connect(self.on_listed_task_finished) # global_signals.dialog_finished.connect(self.on_dialog_finished) # global_signals.open_login_dialog.connect(self.show_login_dialog) - # # --- Start background --- + # --- Background tasks --- # start_background_listed() # --- Store opened dialogs --- self.login_dialogs = [] - def show_login_dialog(self, account_id, listed_id): - print(account_id, listed_id) + # ---------------------------------------------------------------------- + def show_login_dialog(self, account_id: int, listed_id: int): + """Hiển thị dialog xử lý login (modeless)""" + print(f"[INFO] Open LoginHandleDialog for acc={account_id}, listed={listed_id}") dialog = LoginHandleDialog(account_id=account_id, listed_id=listed_id) - dialog.show() # modeless + dialog.setParent(self) # đảm bảo dialog đóng theo MainWindow + dialog.show() self.login_dialogs.append(dialog) - # Khi dialog đóng, remove khỏi list và SharedStore - def on_dialog_close(): - if dialog in self.login_dialogs: - self.login_dialogs.remove(dialog) - self.on_dialog_finished(account_id, listed_id) + # Cleanup khi dialog đóng + dialog.finished.connect( + lambda: self._on_dialog_closed(dialog, account_id, listed_id) + ) - dialog.finished.connect(on_dialog_close) + # ---------------------------------------------------------------------- + def _on_dialog_closed(self, dialog, account_id, listed_id): + """Xử lý khi dialog đóng""" + if dialog in self.login_dialogs: + self.login_dialogs.remove(dialog) + self.on_dialog_finished(account_id, listed_id) + # ---------------------------------------------------------------------- def on_dialog_finished(self, account_id, listed_id): - """Dialog xong, remove khỏi store""" + """Khi dialog xử lý xong""" store = SharedStore.get_instance() store.remove(listed_id) - def on_tab_changed(self, index): + # ---------------------------------------------------------------------- + def on_tab_changed(self, index: int): + """Lazy-load dữ liệu khi đổi tab""" tab = self.tabs.widget(index) if hasattr(tab, "load_data") and not getattr(tab, "is_loaded", False): tab.load_data() tab.is_loaded = True + # ---------------------------------------------------------------------- def on_listed_task_finished(self): + """Khi task nền hoàn tất""" QMessageBox.information( - self, - "Auto Listing", - "All pending listed items have been processed!" + self, "Auto Listing", "All pending listed items have been processed!" ) + # ---------------------------------------------------------------------- def closeEvent(self, event): - """Khi MainWindow đóng, đóng hết tất cả dialog đang mở""" - for dialog in self.login_dialogs[:]: # copy list để tránh lỗi khi remove + """Khi đóng MainWindow -> đóng hết dialog con""" + for dialog in self.login_dialogs[:]: # copy để tránh modify khi lặp dialog.close() event.accept() diff --git a/gui/tabs/accounts/account_tab.py b/gui/tabs/accounts/account_tab.py index 019de5f..fcc1846 100644 --- a/gui/tabs/accounts/account_tab.py +++ b/gui/tabs/accounts/account_tab.py @@ -1,22 +1,30 @@ import os from functools import partial -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, - QPushButton, QHBoxLayout, QDialog, QLabel, QLineEdit, QComboBox, QMessageBox, - QMenu, QAction, QSizePolicy +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QTableWidget, + QTableWidgetItem, + QPushButton, + QHBoxLayout, + QMessageBox, + QMenu, + QSizePolicy, + QHeaderView, ) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import QHeaderView +from PyQt6.QtGui import QAction, QFont +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont from database.models import Account from .forms.account_form import AccountForm from config import PROFILES_DIR # 👇 import cửa sổ login FB -from gui.handle.login_fb import LoginFB # chỉnh path này theo project của bạn +from gui.handle.login_fb import LoginFB # chỉnh path này theo project của bạn PAGE_SIZE = 10 + class AccountTab(QWidget): def __init__(self): super().__init__() @@ -31,7 +39,9 @@ class AccountTab(QWidget): # 🆕 Action menu self.options_btn = QPushButton("Action") - self.options_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.options_btn.setSizePolicy( + QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed + ) self.options_btn.setMinimumWidth(80) self.options_btn.setMaximumWidth(120) top_layout.addWidget(self.options_btn) @@ -40,7 +50,9 @@ class AccountTab(QWidget): # Table self.table = QTableWidget() self.table.verticalHeader().setDefaultSectionSize(28) # row gọn - self.table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.table.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) layout.addWidget(self.table) # Pagination @@ -68,16 +80,22 @@ class AccountTab(QWidget): self.table.setRowCount(len(page_items)) self.table.setColumnCount(6) - self.table.setHorizontalHeaderLabels([ - "ID", "Email", "Status", "Profile Exists", "Login At", "Actions" - ]) + self.table.setHorizontalHeaderLabels( + ["ID", "Email", "Status", "Profile Exists", "Login At", "Actions"] + ) for i, acc in enumerate(page_items): acc_dict = {k: acc[k] for k in acc.keys()} self.table.setItem(i, 0, QTableWidgetItem(str(acc_dict["id"]))) self.table.setItem(i, 1, QTableWidgetItem(acc_dict["email"])) - self.table.setItem(i, 2, QTableWidgetItem("Active" if acc_dict["is_active"] == 1 else "Inactive")) + self.table.setItem( + i, + 2, + QTableWidgetItem( + "Active" if acc_dict["is_active"] == 1 else "Inactive" + ), + ) # ✅ Check profile folder folder_name = acc_dict["email"] @@ -95,7 +113,9 @@ class AccountTab(QWidget): # 🆕 Login action action_login = QAction("Login", btn_menu) - action_login.triggered.connect(lambda _, a=acc_dict: self.open_login_window(a)) + action_login.triggered.connect( + lambda _, a=acc_dict: self.open_login_window(a) + ) menu.addAction(action_login) # Edit @@ -105,7 +125,9 @@ class AccountTab(QWidget): # Delete action_delete = QAction("Delete", btn_menu) - action_delete.triggered.connect(lambda _, a=acc_dict: self.delete_account(a)) + action_delete.triggered.connect( + lambda _, a=acc_dict: self.delete_account(a) + ) menu.addAction(action_delete) btn_menu.setMenu(menu) @@ -113,12 +135,12 @@ class AccountTab(QWidget): # Column sizing header = self.table.horizontalHeader() - header.setSectionResizeMode(0, QHeaderView.ResizeToContents) - header.setSectionResizeMode(1, QHeaderView.Stretch) - header.setSectionResizeMode(2, QHeaderView.ResizeToContents) - header.setSectionResizeMode(3, QHeaderView.ResizeToContents) - header.setSectionResizeMode(4, QHeaderView.ResizeToContents) - header.setSectionResizeMode(5, QHeaderView.Fixed) + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) self.table.setColumnWidth(5, 100) self.prev_btn.setEnabled(self.current_page > 0) @@ -134,21 +156,23 @@ class AccountTab(QWidget): def add_account(self): form = AccountForm(self) - if form.exec_(): + if form.exec(): self.current_page = 0 self.load_data() def edit_account(self, account): form = AccountForm(self, account) - if form.exec_(): + if form.exec(): self.load_data() def delete_account(self, account): confirm = QMessageBox.question( - self, "Confirm", f"Delete account {account['email']}?", - QMessageBox.Yes | QMessageBox.No + self, + "Confirm", + f"Delete account {account['email']}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if confirm == QMessageBox.Yes: + if confirm == QMessageBox.StandardButton.Yes: Account.delete(account["id"]) self.load_data() @@ -162,5 +186,7 @@ class AccountTab(QWidget): # 🆕 Mở cửa sổ login FB def open_login_window(self, account): - self.login_window = LoginFB(account) # truyền account vào nếu class LoginFB có nhận + self.login_window = LoginFB( + account + ) # truyền account vào nếu class LoginFB có nhận self.login_window.show() diff --git a/gui/tabs/accounts/forms/account_form.py b/gui/tabs/accounts/forms/account_form.py index 858767b..538b10a 100644 --- a/gui/tabs/accounts/forms/account_form.py +++ b/gui/tabs/accounts/forms/account_form.py @@ -1,9 +1,18 @@ -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QComboBox, - QPushButton, QMessageBox +# gui/dialogs/account_form.py +from PyQt6.QtWidgets import ( + QDialog, + QVBoxLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QComboBox, + QPushButton, + QMessageBox, ) +from PyQt6.QtCore import Qt from database.models.account import Account + class AccountForm(QDialog): def __init__(self, parent=None, account=None): super().__init__(parent) @@ -23,7 +32,7 @@ class AccountForm(QDialog): layout.addWidget(QLabel("Password")) pw_layout = QHBoxLayout() self.password_input = QLineEdit() - self.password_input.setEchoMode(QLineEdit.Password) + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) self.password_input.setMinimumWidth(200) self.toggle_btn = QPushButton("Show") @@ -50,7 +59,7 @@ class AccountForm(QDialog): self.cancel_btn = QPushButton("Cancel") self.cancel_btn.setMinimumWidth(80) - self.cancel_btn.clicked.connect(self.close) + self.cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(self.cancel_btn) layout.addLayout(btn_layout) @@ -60,21 +69,29 @@ class AccountForm(QDialog): if account: self.email_input.setText(account.get("email", "")) self.password_input.setText(account.get("password", "")) - self.active_input.setCurrentText("Active" if account.get("is_active", 1) == 1 else "Inactive") + self.active_input.setCurrentText( + "Active" if account.get("is_active", 1) == 1 else "Inactive" + ) def toggle_password(self): """Ẩn/hiện password khi nhấn nút""" if self.toggle_btn.isChecked(): - self.password_input.setEchoMode(QLineEdit.Normal) + self.password_input.setEchoMode(QLineEdit.EchoMode.Normal) self.toggle_btn.setText("Hide") else: - self.password_input.setEchoMode(QLineEdit.Password) + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) self.toggle_btn.setText("Show") def save(self): - email = self.email_input.text() - password = self.password_input.text() + """Lưu hoặc cập nhật account""" + email = self.email_input.text().strip() + password = self.password_input.text().strip() is_active = 1 if self.active_input.currentText() == "Active" else 0 + + if not email or not password: + QMessageBox.warning(self, "Error", "Email và Password không được để trống.") + return + try: if self.account and "id" in self.account: Account.update(self.account["id"], email, password, is_active) @@ -82,4 +99,4 @@ class AccountForm(QDialog): Account.create(email, password, is_active) self.accept() except Exception as e: - QMessageBox.warning(self, "Error", str(e)) + QMessageBox.critical(self, "Error", f"Failed to save account:\n{e}") diff --git a/gui/tabs/import_tab.py b/gui/tabs/import_tab.py index ee06723..6914ca4 100644 --- a/gui/tabs/import_tab.py +++ b/gui/tabs/import_tab.py @@ -3,9 +3,15 @@ import time from services.core.loading_service import run_with_progress from database.models.product import Product -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QPushButton, QFileDialog, - QTableWidget, QTableWidgetItem, QHBoxLayout, QMessageBox +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QPushButton, + QFileDialog, + QTableWidget, + QTableWidgetItem, + QHBoxLayout, + QMessageBox, ) @@ -40,13 +46,15 @@ class ImportTab(QWidget): self.preview_data = [] # store imported data def import_csv(self): - file_path, _ = QFileDialog.getOpenFileName(self, "Select CSV File", "", "CSV Files (*.csv)") + file_path, _ = QFileDialog.getOpenFileName( + self, "Select CSV File", "", "CSV Files (*.csv)" + ) if not file_path: return try: # đọc CSV chuẩn với DictReader - with open(file_path, newline='', encoding="utf-8-sig") as csvfile: + with open(file_path, newline="", encoding="utf-8-sig") as csvfile: reader = csv.DictReader(csvfile) headers = reader.fieldnames rows = list(reader) @@ -75,7 +83,9 @@ class ImportTab(QWidget): QMessageBox.critical(self, "Error", f"Failed to read CSV: {e}") def import_api(self): - QMessageBox.information(self, "Info", "API import feature will be developed later 😉") + QMessageBox.information( + self, "Info", "API import feature will be developed later 😉" + ) def save_to_db(self): if not self.preview_data: @@ -87,14 +97,13 @@ class ImportTab(QWidget): self, "Confirm Import", f"Are you sure you want to import {len(self.preview_data)} rows?", - QMessageBox.Yes | QMessageBox.No + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if reply != QMessageBox.Yes: + if reply != QMessageBox.StandardButton.Yes: return def handler(item): try: - # time.sleep(0.05) # có thể bỏ nếu không cần debug progress Product.insert_from_import(item) return True except Exception as e: @@ -102,16 +111,13 @@ class ImportTab(QWidget): return False success, fail = run_with_progress( - self.preview_data, - handler=handler, - message="Importing data...", - parent=self + self.preview_data, handler=handler, message="Importing data...", parent=self ) QMessageBox.information( self, "Import Completed", - f"Successfully imported {success}/{len(self.preview_data)} rows.\nFailed: {fail} rows." + f"Successfully imported {success}/{len(self.preview_data)} rows.\nFailed: {fail} rows.", ) # ✅ Clear preview sau khi import xong diff --git a/gui/tabs/listeds/dialogs/listed_filter_dialog.py b/gui/tabs/listeds/dialogs/listed_filter_dialog.py index 3b712fe..820786b 100644 --- a/gui/tabs/listeds/dialogs/listed_filter_dialog.py +++ b/gui/tabs/listeds/dialogs/listed_filter_dialog.py @@ -1,10 +1,16 @@ -from PyQt5.QtWidgets import ( - QDialog, QFormLayout, QLineEdit, QDialogButtonBox, - QVBoxLayout, QWidget, QComboBox +from PyQt6.QtWidgets import ( + QDialog, + QFormLayout, + QLineEdit, + QDialogButtonBox, + QVBoxLayout, + QWidget, + QComboBox, ) + class ListedFilterDialog(QDialog): - STATUS_OPTIONS = ["Any", "pending", "listed"] # ✅ thêm 'Any' để không lọc theo trạng thái + STATUS_OPTIONS = ["Any", "pending", "listed"] def __init__(self, parent=None): super().__init__(parent) @@ -27,15 +33,17 @@ class ListedFilterDialog(QDialog): self.account_input = QLineEdit() form_layout.addRow("Account Email:", self.account_input) - # Status (dropdown) + # Status self.status_input = QComboBox() self.status_input.addItems(self.STATUS_OPTIONS) form_layout.addRow("Status:", self.status_input) main_layout.addWidget(form_widget) - # Buttons - self.btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + # Buttons (PyQt6 khác cú pháp một chút) + self.btn_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) self.btn_box.accepted.connect(self.accept) self.btn_box.rejected.connect(self.reject) main_layout.addWidget(self.btn_box) diff --git a/gui/tabs/listeds/listed_tab.py b/gui/tabs/listeds/listed_tab.py index 15bbb6f..df208b3 100644 --- a/gui/tabs/listeds/listed_tab.py +++ b/gui/tabs/listeds/listed_tab.py @@ -1,12 +1,27 @@ +# gui/tabs/listeds/listed_tab.py from functools import partial import json -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, - QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView, - QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton, - QStyle, QStylePainter, QLineEdit, QProgressBar +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QTableWidget, + QTableWidgetItem, + QPushButton, + QHBoxLayout, + QMenu, + QHeaderView, + QLabel, + QCheckBox, + QSizePolicy, + QMessageBox, + QStyleOptionButton, + QStyle, + QStylePainter, + QLineEdit, + QProgressBar, ) -from PyQt5.QtCore import Qt, QRect, pyqtSignal, QTimer +from PyQt6.QtGui import QAction +from PyQt6.QtCore import Qt, QRect, pyqtSignal, QTimer from services.core.loading_service import run_with_progress from services.image_service import ImageService from database.models.listed import Listed @@ -15,6 +30,7 @@ from gui.tabs.listeds.dialogs.listed_filter_dialog import ListedFilterDialog PAGE_SIZE = 10 + # --- Header Checkbox --- class CheckBoxHeader(QHeaderView): select_all_changed = pyqtSignal(bool) @@ -33,9 +49,11 @@ class CheckBoxHeader(QHeaderView): x = rect.x() + (rect.width() - size) // 2 y = rect.y() + (rect.height() - size) // 2 option.rect = QRect(x, y, size, size) - option.state = QStyle.State_Enabled | (QStyle.State_On if self.isOn else QStyle.State_Off) + option.state = QStyle.StateFlag.State_Enabled | ( + QStyle.StateFlag.State_On if self.isOn else QStyle.StateFlag.State_Off + ) painter2 = QStylePainter(self.viewport()) - painter2.drawControl(QStyle.CE_CheckBox, option) + painter2.drawControl(QStyle.ControlElement.CE_CheckBox, option) def handle_section_pressed(self, logicalIndex): if logicalIndex == 0: @@ -46,11 +64,7 @@ class CheckBoxHeader(QHeaderView): # --- ListedTab --- class ListedTab(QWidget): - SORTABLE_COLUMNS = { - 1: "id", - 3: "product_name", - 5: "listed_at" - } + SORTABLE_COLUMNS = {1: "id", 3: "product_name", 5: "listed_at"} def __init__(self): super().__init__() @@ -63,43 +77,47 @@ class ListedTab(QWidget): layout = QVBoxLayout() - # Top menu + # --- Top layout --- top_layout = QHBoxLayout() - top_layout.addStretch() # placeholder stretch + top_layout.addStretch() - # --- Progress bar --- + # Progress bar self.progress_bar = QProgressBar() self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(100) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(True) self.progress_bar.setMinimumWidth(150) - self.progress_bar.setMinimumHeight(25) # thêm chiều cao đủ lớn - self.progress_bar.setAlignment(Qt.AlignCenter) # căn giữa text + self.progress_bar.setMinimumHeight(25) + self.progress_bar.setAlignment(Qt.AlignmentFlag.AlignCenter) top_layout.insertWidget(0, self.progress_bar) - top_layout.addStretch() # đẩy Action button sang phải + top_layout.addStretch() - # --- Action button --- + # Action button self.options_btn = QPushButton("Action") - self.options_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + # QSizePolicy.Policy for PyQt6 + self.options_btn.setSizePolicy( + QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed + ) self.options_btn.setMinimumWidth(50) self.options_btn.setMaximumWidth(120) top_layout.addWidget(self.options_btn) layout.addLayout(top_layout) - # Table + # --- Table --- self.table = QTableWidget() self.table.verticalHeader().setDefaultSectionSize(60) - self.table.setEditTriggers(QTableWidget.NoEditTriggers) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - header = CheckBoxHeader(Qt.Horizontal, self.table) + self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + + header = CheckBoxHeader(Qt.Orientation.Horizontal, self.table) self.table.setHorizontalHeader(header) header.select_all_changed.connect(self.select_all_rows) header.sectionClicked.connect(self.handle_header_click) layout.addWidget(self.table) - # Pagination + # --- Pagination --- pag_layout = QHBoxLayout() self.prev_btn = QPushButton("Previous") self.prev_btn.clicked.connect(self.prev_page) @@ -117,14 +135,14 @@ class ListedTab(QWidget): self.next_btn = QPushButton("Next") self.next_btn.clicked.connect(self.next_page) pag_layout.addWidget(self.next_btn) - layout.addLayout(pag_layout) + layout.addLayout(pag_layout) self.setLayout(layout) - # --- Timer để update progress --- + # --- Timer --- self.listing_timer = QTimer() self.listing_timer.timeout.connect(self.update_listed_progress) - self.listing_timer.start(1000) # mỗi giây update + self.listing_timer.start(1000) # --- Load Data --- def load_data(self, show_progress=True): @@ -133,20 +151,34 @@ class ListedTab(QWidget): offset = self.current_page * PAGE_SIZE page_items, total_count = Listed.get_paginated( - offset, PAGE_SIZE, self.filters, - sort_by=self.sort_by, sort_order=self.sort_order + offset, + PAGE_SIZE, + self.filters, + sort_by=self.sort_by, + sort_order=self.sort_order, ) self.total_count = total_count self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE) self.table.setColumnCount(9) - columns = ["", "Image", "SKU", "Product Name", "Account", "Listed At", "Condition", "Status", "Actions"] + columns = [ + "", + "Image", + "SKU", + "Product Name", + "Account", + "Listed At", + "Condition", + "Status", + "Actions", + ] self.table.setHorizontalHeaderLabels(columns) self.table.setRowCount(len(page_items)) def handler(item, i_row): listed_id = item.get("id") + # Checkbox cb = QCheckBox() cb.setProperty("listed_id", listed_id) @@ -160,32 +192,41 @@ class ListedTab(QWidget): if pixmap: lbl = QLabel() lbl.setPixmap(pixmap) - lbl.setAlignment(Qt.AlignCenter) + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setCellWidget(i_row, 1, lbl) else: self.table.setItem(i_row, 1, QTableWidgetItem("None")) else: self.table.setItem(i_row, 1, QTableWidgetItem("None")) - # SKU, Product Name, Account + # Basic info self.table.setItem(i_row, 2, QTableWidgetItem(item.get("sku") or "")) - self.table.setItem(i_row, 3, QTableWidgetItem(item.get("product_name") or "")) - self.table.setItem(i_row, 4, QTableWidgetItem(item.get("account_email") or "")) + self.table.setItem( + i_row, 3, QTableWidgetItem(item.get("product_name") or "") + ) + self.table.setItem( + i_row, 4, QTableWidgetItem(item.get("account_email") or "") + ) # Listed At listed_str = "" ts = item.get("listed_at") if ts: from datetime import datetime + try: - listed_str = datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M") + listed_str = datetime.fromtimestamp(int(ts)).strftime( + "%Y-%m-%d %H:%M" + ) except Exception: listed_str = str(ts) self.table.setItem(i_row, 5, QTableWidgetItem(listed_str)) # Condition, Status self.table.setItem(i_row, 6, QTableWidgetItem(item.get("condition") or "")) - self.table.setItem(i_row, 7, QTableWidgetItem(item.get("status") or "pending")) + self.table.setItem( + i_row, 7, QTableWidgetItem(item.get("status") or "pending") + ) # Actions btn_menu = QPushButton("Actions") @@ -198,25 +239,32 @@ class ListedTab(QWidget): items_with_index = [(p, i) for i, p in enumerate(page_items)] if show_progress: - run_with_progress(items_with_index, handler=lambda x: handler(*x), message="Loading listed...", parent=self) + run_with_progress( + items_with_index, + handler=lambda x: handler(*x), + message="Loading listed...", + parent=self, + ) else: for item in items_with_index: handler(*item) - # Header sizing + # Header resize header = self.table.horizontalHeader() - header.setSectionResizeMode(0, QHeaderView.ResizeToContents) - header.setSectionResizeMode(1, QHeaderView.Fixed) + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) self.table.setColumnWidth(1, 60) for idx in range(2, 8): - header.setSectionResizeMode(idx, QHeaderView.Stretch) - header.setSectionResizeMode(8, QHeaderView.Fixed) + header.setSectionResizeMode(idx, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed) self.table.setColumnWidth(8, 100) - # Pagination + # Pagination info self.prev_btn.setEnabled(self.current_page > 0) self.next_btn.setEnabled(self.current_page < self.total_pages - 1) - self.page_info_label.setText(f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)") + self.page_info_label.setText( + f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)" + ) # Reset header checkbox if isinstance(header, CheckBoxHeader): @@ -247,39 +295,59 @@ class ListedTab(QWidget): menu.addAction(action_clear) # Toggle Auto Listing - auto_setting = Setting.get_by_key(Setting.AUTO_LISTING) + auto_setting = None + try: + auto_setting = Setting.get_by_key(Setting.AUTO_LISTING) + except Exception: + auto_setting = None + if auto_setting: - setting_id = auto_setting["id"] - auto_listing_val = auto_setting["value"].lower() == "true" + setting_id = auto_setting.get("id") + auto_listing_val = str(auto_setting.get("value", "")).lower() == "true" else: - auto_setting = Setting.create(Setting.AUTO_LISTING, "false") - setting_id = auto_setting["id"] - auto_listing_val = False + # create default if missing + try: + created = Setting.create(Setting.AUTO_LISTING, "false") + setting_id = created.get("id") + auto_listing_val = False + except Exception: + setting_id = None + auto_listing_val = False action_toggle_auto = QAction( "Turn Auto Listing OFF" if auto_listing_val else "Turn Auto Listing ON", - menu + menu, ) def toggle_auto_listing(): + if setting_id is None: + QMessageBox.critical( + self, "Error", "AUTO_LISTING setting not available" + ) + return new_val = "false" if auto_listing_val else "true" try: Setting.update_value(setting_id, new_val) QMessageBox.information( - self, "Auto Listing", - f"AUTO_LISTING set to {new_val.upper()}" + self, "Auto Listing", f"AUTO_LISTING set to {new_val.upper()}" ) + # refresh menu and progress self.update_options_menu() self.update_listed_progress() except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to update AUTO_LISTING: {e}") + QMessageBox.critical( + self, "Error", f"Failed to update AUTO_LISTING: {e}" + ) action_toggle_auto.triggered.connect(toggle_auto_listing) menu.addAction(action_toggle_auto) # Delete Selected - if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked() - for i in range(self.table.rowCount())): + if any( + isinstance(self.table.cellWidget(i, 0), QCheckBox) + and self.table.cellWidget(i, 0).isChecked() + for i in range(self.table.rowCount()) + ): action_delete_selected = QAction("Delete Selected", menu) action_delete_selected.triggered.connect(self.delete_selected) menu.addAction(action_delete_selected) @@ -287,7 +355,9 @@ class ListedTab(QWidget): self.options_btn.setMenu(menu) def update_listed_progress(self): - auto_listing_val = Setting.get(Setting.AUTO_LISTING, "false").lower() == "true" + auto_listing_val = ( + str(Setting.get(Setting.AUTO_LISTING, "false")).lower() == "true" + ) self.progress_bar.setVisible(auto_listing_val) if not auto_listing_val: return @@ -311,11 +381,10 @@ class ListedTab(QWidget): self.progress_bar.setTextVisible(True) self.progress_bar.setFormat("0/0 listed") - # --- Filter --- def open_filter_dialog(self): dialog = ListedFilterDialog(self) - if dialog.exec_(): + if dialog.exec(): # PyQt6: exec() self.filters = dialog.get_filters() self.current_page = 0 self.load_data() @@ -366,29 +435,48 @@ class ListedTab(QWidget): # --- Delete --- def delete_selected(self): - ids = [int(cb.property("listed_id")) for i in range(self.table.rowCount()) - if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked()] + ids = [ + int(cb.property("listed_id")) + for i in range(self.table.rowCount()) + if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) + and cb.isChecked() + ] if not ids: QMessageBox.information(self, "Info", "No listed selected") return confirm = QMessageBox.question( - self, "Confirm Delete", f"Delete {len(ids)} selected listed items?", - QMessageBox.Yes | QMessageBox.No + self, + "Confirm Delete", + f"Delete {len(ids)} selected listed items?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if confirm != QMessageBox.Yes: + if confirm != QMessageBox.StandardButton.Yes: return - run_with_progress(ids, handler=lambda x: Listed.bulk_delete([x]), message="Deleting listed...", parent=self) + run_with_progress( + ids, + handler=lambda x: Listed.bulk_delete([x]), + message="Deleting listed...", + parent=self, + ) self.current_page = 0 self.load_data() def delete_listed(self, listed_id): confirm = QMessageBox.question( - self, "Confirm Delete", f"Delete listed ID {listed_id}?", QMessageBox.Yes | QMessageBox.No + self, + "Confirm Delete", + f"Delete listed ID {listed_id}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if confirm != QMessageBox.Yes: + if confirm != QMessageBox.StandardButton.Yes: return - run_with_progress([listed_id], handler=lambda x: Listed.bulk_delete([x]), message="Deleting listed...", parent=self) + run_with_progress( + [listed_id], + handler=lambda x: Listed.bulk_delete([x]), + message="Deleting listed...", + parent=self, + ) self.load_data() diff --git a/gui/tabs/products/dialogs/add_listed_dialog.py b/gui/tabs/products/dialogs/add_listed_dialog.py index 552267f..f4d85c5 100644 --- a/gui/tabs/products/dialogs/add_listed_dialog.py +++ b/gui/tabs/products/dialogs/add_listed_dialog.py @@ -1,8 +1,9 @@ -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QComboBox, QPushButton, QMessageBox +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QComboBox, QPushButton, QMessageBox from services.core.loading_service import run_with_progress from database.models.listed import Listed from database.models.account import Account # import model + class AddListedDialog(QDialog): def __init__(self, product_ids, parent=None): super().__init__(parent) @@ -27,6 +28,7 @@ class AddListedDialog(QDialog): btn_ok = QPushButton("Add Listed") btn_ok.clicked.connect(self.process_add_listed) layout.addWidget(btn_ok) + self.setLayout(layout) def process_add_listed(self): @@ -40,16 +42,20 @@ class AddListedDialog(QDialog): return confirm = QMessageBox.question( - self, "Confirm Add Listed", + self, + "Confirm Add Listed", f"Add {len(self.product_ids)} product(s) to listed under selected account?", - QMessageBox.Yes | QMessageBox.No + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if confirm != QMessageBox.Yes: + + if confirm != QMessageBox.StandardButton.Yes: return def handler(product_id): try: - Listed.bulk_create([{"product_id": product_id, "account_id": self.selected_account}]) + Listed.bulk_create( + [{"product_id": product_id, "account_id": self.selected_account}] + ) except Exception as e: print(f"Error adding listed for product {product_id}: {e}") @@ -57,8 +63,10 @@ class AddListedDialog(QDialog): self.product_ids, handler=handler, message="Adding listed...", - parent=self + parent=self, ) - QMessageBox.information(self, "Success", f"Added {len(self.product_ids)} product(s) to listed.") + QMessageBox.information( + self, "Success", f"Added {len(self.product_ids)} product(s) to listed." + ) self.accept() diff --git a/gui/tabs/products/dialogs/product_filter_dialog.py b/gui/tabs/products/dialogs/product_filter_dialog.py index e53e2d9..6bb9097 100644 --- a/gui/tabs/products/dialogs/product_filter_dialog.py +++ b/gui/tabs/products/dialogs/product_filter_dialog.py @@ -1,9 +1,16 @@ -from PyQt5.QtWidgets import ( - QDialog, QFormLayout, QLineEdit, QDateEdit, QComboBox, - QDialogButtonBox, QPushButton, QHBoxLayout, QVBoxLayout, QWidget +from PyQt6.QtWidgets import ( + QDialog, + QFormLayout, + QLineEdit, + QDateEdit, + QComboBox, + QDialogButtonBox, + QHBoxLayout, + QVBoxLayout, + QWidget, ) -from PyQt5.QtCore import QDate -import json +from PyQt6.QtCore import QDate + class FilterDialog(QDialog): SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"] @@ -62,11 +69,12 @@ class FilterDialog(QDialog): right_form.addRow("Location:", self.location_input) columns_layout.addWidget(right_widget) - main_layout.addLayout(columns_layout) # --- Button Box --- - self.btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.btn_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) self.btn_box.accepted.connect(self.accept) self.btn_box.rejected.connect(self.reject) main_layout.addWidget(self.btn_box) diff --git a/gui/tabs/products/forms/product_form.py b/gui/tabs/products/forms/product_form.py index eb39e2a..8732d00 100644 --- a/gui/tabs/products/forms/product_form.py +++ b/gui/tabs/products/forms/product_form.py @@ -1,17 +1,30 @@ import json -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QComboBox, QTextEdit, QPushButton, QGridLayout, - QFileDialog, QMessageBox +from PyQt6.QtWidgets import ( + QDialog, + QVBoxLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QComboBox, + QTextEdit, + QPushButton, + QGridLayout, + QFileDialog, + QMessageBox, ) -from PyQt5.QtCore import Qt +from PyQt6.QtCore import Qt from database.models.product import Product class ProductForm(QDialog): SAMPLE_NAMES = [ - "Apple iPhone 15", "Samsung Galaxy S23", "Sony Headphones", - "Dell Laptop", "Canon Camera", "Nike Shoes", "Adidas T-Shirt" + "Apple iPhone 15", + "Samsung Galaxy S23", + "Sony Headphones", + "Dell Laptop", + "Canon Camera", + "Nike Shoes", + "Adidas T-Shirt", ] SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"] SAMPLE_CONDITIONS = ["New", "Used", "Refurbished"] @@ -116,7 +129,11 @@ class ProductForm(QDialog): self, "Select Image Files", "", "Images (*.png *.jpg *.jpeg *.bmp)" ) if files: - existing = [img.strip() for img in self.images_input.toPlainText().split(",") if img.strip()] + existing = [ + img.strip() + for img in self.images_input.toPlainText().split(",") + if img.strip() + ] self.images_input.setText(",".join(existing + files)) def save(self): @@ -131,35 +148,61 @@ class ProductForm(QDialog): QMessageBox.warning(self, "Error", "Price must be a valid number") return - category = self.category_input.currentText() if self.category_input.currentText() != "None" else None - condition = self.condition_input.currentText() if self.condition_input.currentText() != "None" else None + category = ( + self.category_input.currentText() + if self.category_input.currentText() != "None" + else None + ) + condition = ( + self.condition_input.currentText() + if self.condition_input.currentText() != "None" + else None + ) brand = self.brand_input.text().strip() or None description = self.description_input.toPlainText().strip() or None - tags = [t.strip() for t in self.tags_input.text().split(",") if t.strip()] or None + tags = [ + t.strip() for t in self.tags_input.text().split(",") if t.strip() + ] or None sku = self.sku_input.text().strip() or None location = self.location_input.text().strip() or None - # Xử lý images: remove dấu ngoặc và quote nếu copy từ JSON raw_text = self.images_input.toPlainText() - raw_text = raw_text.replace('[', '').replace(']', '').replace('"', '').replace("'", '') + raw_text = ( + raw_text.replace("[", "").replace("]", "").replace('"', "").replace("'", "") + ) images = [img.strip() for img in raw_text.split(",") if img.strip()] try: if self.product and "id" in self.product: Product.update( - self.product["id"], name, price, + self.product["id"], + name, + price, images=images, - url=None, status="draft", - category=category, condition=condition, brand=brand, - description=description, tags=tags, sku=sku, location=location + url=None, + status="draft", + category=category, + condition=condition, + brand=brand, + description=description, + tags=tags, + sku=sku, + location=location, ) else: Product.create( - name, price, + name, + price, images=images, - url=None, status="draft", - category=category, condition=condition, brand=brand, - description=description, tags=tags, sku=sku, location=location + url=None, + status="draft", + category=category, + condition=condition, + brand=brand, + description=description, + tags=tags, + sku=sku, + location=location, ) self.accept() except Exception as e: diff --git a/gui/tabs/products/product_tab.py b/gui/tabs/products/product_tab.py index e32d88b..099d569 100644 --- a/gui/tabs/products/product_tab.py +++ b/gui/tabs/products/product_tab.py @@ -1,14 +1,27 @@ import json from functools import partial from services.core.loading_service import run_with_progress -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, - QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView, - QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton, QStyle, QStylePainter, QLineEdit +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QTableWidget, + QTableWidgetItem, + QPushButton, + QHBoxLayout, + QMenu, + QHeaderView, + QLabel, + QCheckBox, + QSizePolicy, + QMessageBox, + QStyleOptionButton, + QStyle, + QStylePainter, + QLineEdit, ) -from PyQt5.QtCore import Qt, QRect, pyqtSignal +from PyQt6.QtGui import QAction +from PyQt6.QtCore import Qt, QRect, pyqtSignal from database.models.product import Product -from database.models.listed import Listed from services.image_service import ImageService from gui.tabs.products.forms.product_form import ProductForm from gui.tabs.products.dialogs.product_filter_dialog import FilterDialog @@ -16,6 +29,7 @@ from gui.tabs.products.dialogs.add_listed_dialog import AddListedDialog PAGE_SIZE = 10 + # --- Header Checkbox --- class CheckBoxHeader(QHeaderView): select_all_changed = pyqtSignal(bool) @@ -34,9 +48,14 @@ class CheckBoxHeader(QHeaderView): x = rect.x() + (rect.width() - size) // 2 y = rect.y() + (rect.height() - size) // 2 option.rect = QRect(x, y, size, size) - option.state = QStyle.State_Enabled | (QStyle.State_On if self.isOn else QStyle.State_Off) + # PyQt6: use QStyle.StateFlag and QStyle.ControlElement + state = QStyle.StateFlag.State_Enabled + state = state | ( + QStyle.StateFlag.State_On if self.isOn else QStyle.StateFlag.State_Off + ) + option.state = state painter2 = QStylePainter(self.viewport()) - painter2.drawControl(QStyle.CE_CheckBox, option) + painter2.drawControl(QStyle.ControlElement.CE_CheckBox, option) def handle_section_pressed(self, logicalIndex): if logicalIndex == 0: @@ -47,12 +66,7 @@ class CheckBoxHeader(QHeaderView): # --- ProductTab --- class ProductTab(QWidget): - SORTABLE_COLUMNS = { - 1: "id", - 3: "name", - 4: "price", - 7: "created_at" - } + SORTABLE_COLUMNS = {1: "id", 3: "name", 4: "price", 7: "created_at"} def __init__(self): super().__init__() @@ -72,7 +86,9 @@ class ProductTab(QWidget): top_layout.addWidget(self.add_btn) self.options_btn = QPushButton("Action") - self.options_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.options_btn.setSizePolicy( + QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed + ) self.options_btn.setMinimumWidth(50) self.options_btn.setMaximumWidth(120) top_layout.addWidget(self.options_btn) @@ -81,10 +97,10 @@ class ProductTab(QWidget): # Table self.table = QTableWidget() self.table.verticalHeader().setDefaultSectionSize(60) - self.table.setEditTriggers(QTableWidget.NoEditTriggers) - self.table.setSelectionBehavior(QTableWidget.SelectRows) + self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) - header = CheckBoxHeader(Qt.Horizontal, self.table) + header = CheckBoxHeader(Qt.Orientation.Horizontal, self.table) self.table.setHorizontalHeader(header) header.select_all_changed.connect(self.select_all_rows) header.sectionClicked.connect(self.handle_header_click) @@ -141,16 +157,22 @@ class ProductTab(QWidget): action_clear = QAction("Clear Filter", menu) action_clear.triggered.connect(self.clear_filters) menu.addAction(action_clear) - + # --- Thêm Add Listed Selected --- - if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked() - for i in range(self.table.rowCount())): + if any( + isinstance(self.table.cellWidget(i, 0), QCheckBox) + and self.table.cellWidget(i, 0).isChecked() + for i in range(self.table.rowCount()) + ): action_add_listed_selected = QAction("Add Listed Selected", menu) action_add_listed_selected.triggered.connect(self.add_listed_selected) menu.addAction(action_add_listed_selected) - if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked() - for i in range(self.table.rowCount())): + if any( + isinstance(self.table.cellWidget(i, 0), QCheckBox) + and self.table.cellWidget(i, 0).isChecked() + for i in range(self.table.rowCount()) + ): action_delete_selected = QAction("Delete Selected", menu) action_delete_selected.triggered.connect(self.delete_selected) menu.addAction(action_delete_selected) @@ -160,7 +182,7 @@ class ProductTab(QWidget): # --- Filter --- def open_filter_dialog(self): dialog = FilterDialog(self) - if dialog.exec_(): + if dialog.exec(): # PyQt6: exec() self.filters = dialog.get_filters() self.current_page = 0 self.load_data() @@ -182,15 +204,28 @@ class ProductTab(QWidget): offset = self.current_page * PAGE_SIZE # Lấy toàn bộ dữ liệu cần load page_items, total_count = Product.get_paginated( - offset, PAGE_SIZE, self.filters, - sort_by=self.sort_by, sort_order=self.sort_order + offset, + PAGE_SIZE, + self.filters, + sort_by=self.sort_by, + sort_order=self.sort_order, ) self.total_count = total_count self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE) self.table.setColumnCount(9) - columns = ["", "Image", "SKU", "Name", "Price", "Condition", "Brand", "Created At", "Actions"] + columns = [ + "", + "Image", + "SKU", + "Name", + "Price", + "Condition", + "Brand", + "Created At", + "Actions", + ] self.table.setHorizontalHeaderLabels(columns) self.table.setRowCount(len(page_items)) @@ -210,7 +245,7 @@ class ProductTab(QWidget): if pixmap: lbl = QLabel() lbl.setPixmap(pixmap) - lbl.setAlignment(Qt.AlignCenter) + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setCellWidget(i_row, 1, lbl) else: self.table.setItem(i_row, 1, QTableWidgetItem("None")) @@ -228,8 +263,11 @@ class ProductTab(QWidget): created_ts = p.get("created_at") if created_ts: from datetime import datetime + try: - created_str = datetime.fromtimestamp(int(created_ts)).strftime("%Y-%m-%d %H:%M") + created_str = datetime.fromtimestamp(int(created_ts)).strftime( + "%Y-%m-%d %H:%M" + ) except Exception: created_str = str(created_ts) self.table.setItem(i_row, 7, QTableWidgetItem(created_str)) @@ -240,11 +278,11 @@ class ProductTab(QWidget): act_edit = QAction("Edit", btn_menu) act_edit.triggered.connect(partial(self.edit_product, p)) menu.addAction(act_edit) - + act_add_listed = QAction("Add Listed", btn_menu) # <-- thêm action này act_add_listed.triggered.connect(partial(self.add_listed_row, product_id)) menu.addAction(act_add_listed) - + act_del = QAction("Delete", btn_menu) act_del.triggered.connect(partial(self.delete_product, product_id)) menu.addAction(act_del) @@ -258,7 +296,7 @@ class ProductTab(QWidget): items_with_index, handler=lambda x: handler(*x), message="Loading products...", - parent=self + parent=self, ) else: for item in items_with_index: @@ -266,18 +304,20 @@ class ProductTab(QWidget): # Header sizing header = self.table.horizontalHeader() - header.setSectionResizeMode(0, QHeaderView.ResizeToContents) - header.setSectionResizeMode(1, QHeaderView.Fixed) + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) self.table.setColumnWidth(1, 60) for idx in range(2, 8): - header.setSectionResizeMode(idx, QHeaderView.Stretch) - header.setSectionResizeMode(8, QHeaderView.Fixed) + header.setSectionResizeMode(idx, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed) self.table.setColumnWidth(8, 100) # Pagination self.prev_btn.setEnabled(self.current_page > 0) self.next_btn.setEnabled(self.current_page < self.total_pages - 1) - self.page_info_label.setText(f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)") + self.page_info_label.setText( + f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)" + ) # Reset header checkbox if isinstance(header, CheckBoxHeader): @@ -286,8 +326,6 @@ class ProductTab(QWidget): self.update_options_menu() - - # --- Go to page --- def go_to_page(self): try: @@ -310,70 +348,72 @@ class ProductTab(QWidget): ids = [ int(cb.property("product_id")) for i in range(self.table.rowCount()) - if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked() + if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) + and cb.isChecked() ] if not ids: QMessageBox.information(self, "Info", "No product selected") return confirm = QMessageBox.question( - self, "Confirm Delete", f"Delete {len(ids)} selected products?", - QMessageBox.Yes | QMessageBox.No + self, + "Confirm Delete", + f"Delete {len(ids)} selected products?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if confirm != QMessageBox.Yes: + if confirm != QMessageBox.StandardButton.Yes: return # --- dùng run_with_progress --- run_with_progress( - ids, - handler=Product.delete, - message="Deleting products...", - parent=self + ids, handler=Product.delete, message="Deleting products...", parent=self ) self.current_page = 0 self.load_data() - # --- Product Actions --- def add_product(self): form = ProductForm(self) - if form.exec_(): + if form.exec(): # PyQt6: exec() self.current_page = 0 self.load_data() def edit_product(self, product): form = ProductForm(self, product) - if form.exec_(): + if form.exec(): # PyQt6: exec() self.load_data() def delete_product(self, product_id): confirm = QMessageBox.question( - self, "Confirm Delete", f"Delete product ID {product_id}?", QMessageBox.Yes | QMessageBox.No + self, + "Confirm Delete", + f"Delete product ID {product_id}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - if confirm != QMessageBox.Yes: + if confirm != QMessageBox.StandardButton.Yes: return run_with_progress( [product_id], handler=Product.delete, message="Deleting product...", - parent=self + parent=self, ) self.load_data() - - + def add_listed_selected(self): selected_ids = [ int(cb.property("product_id")) for i in range(self.table.rowCount()) - if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked() + if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) + and cb.isChecked() ] if not selected_ids: QMessageBox.information(self, "Info", "No products selected") return dialog = AddListedDialog(selected_ids, parent=self) - dialog.exec_() + dialog.exec() # PyQt6 # --- Clear row checkboxes --- for i in range(self.table.rowCount()): @@ -387,7 +427,6 @@ class ProductTab(QWidget): # --- Update menu --- self.update_options_menu() - def reset_header_checkbox(self): header = self.table.horizontalHeader() if isinstance(header, CheckBoxHeader): @@ -401,8 +440,7 @@ class ProductTab(QWidget): return dialog = AddListedDialog([product_id], parent=self) - dialog.exec_() - + dialog.exec() # PyQt6 # --- Pagination --- def next_page(self): diff --git a/gui/tabs/settings/forms/setting_form.py b/gui/tabs/settings/forms/setting_form.py index 8db8963..924cff0 100644 --- a/gui/tabs/settings/forms/setting_form.py +++ b/gui/tabs/settings/forms/setting_form.py @@ -1,9 +1,15 @@ -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QLabel, QLineEdit, - QComboBox, QDialogButtonBox, QMessageBox +from PyQt6.QtWidgets import ( + QDialog, + QVBoxLayout, + QLabel, + QLineEdit, + QComboBox, + QDialogButtonBox, + QMessageBox, ) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIntValidator +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QIntValidator + class SettingForm(QDialog): def __init__(self, key: str, value: str, type_: str = "text", parent=None): @@ -28,7 +34,9 @@ class SettingForm(QDialog): if self.type_ == "boolean" or val_lower in ("true", "false"): self.input_widget = QComboBox() self.input_widget.addItems(["true", "false"]) - self.input_widget.setCurrentText(val_lower if val_lower in ("true", "false") else "true") + self.input_widget.setCurrentText( + val_lower if val_lower in ("true", "false") else "true" + ) else: self.input_widget = QLineEdit() self.input_widget.setText(value or "") @@ -38,18 +46,23 @@ class SettingForm(QDialog): self.layout.addWidget(self.input_widget) # --- Buttons --- - buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) buttons.accepted.connect(self.on_accept) buttons.rejected.connect(self.reject) self.layout.addWidget(buttons) + # ---------------------------------------------------------------------- def on_accept(self): + """Xử lý khi người dùng nhấn OK""" if isinstance(self.input_widget, QLineEdit): val = self.input_widget.text() - if self.type_ == "number": - if not val.isdigit(): - QMessageBox.warning(self, "Invalid input", "Please enter a valid number.") - return + if self.type_ == "number" and not val.isdigit(): + QMessageBox.warning( + self, "Invalid input", "Please enter a valid number." + ) + return elif isinstance(self.input_widget, QComboBox): val = self.input_widget.currentText() else: @@ -62,10 +75,12 @@ class SettingForm(QDialog): self.new_value = val self.accept() + # ---------------------------------------------------------------------- @staticmethod def get_new_value(key: str, value: str, type_: str = "text", parent=None): + """Hiển thị dialog và trả về giá trị mới""" dialog = SettingForm(key, value, type_=type_, parent=parent) - result = dialog.exec_() - if result == QDialog.Accepted: + result = dialog.exec() # ✅ PyQt6 dùng exec() thay vì exec_() + if result == QDialog.DialogCode.Accepted: return dialog.new_value return None diff --git a/gui/tabs/settings/settings_tab.py b/gui/tabs/settings/settings_tab.py index f6ee241..847bb89 100644 --- a/gui/tabs/settings/settings_tab.py +++ b/gui/tabs/settings/settings_tab.py @@ -1,18 +1,25 @@ from functools import partial -import json -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, - QHBoxLayout, QLabel, QPushButton, QMessageBox, QMenu, - QAction, QHeaderView, QSizePolicy, QLineEdit +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QTableWidget, + QTableWidgetItem, + QHBoxLayout, + QLabel, + QPushButton, + QMessageBox, + QMenu, + QHeaderView, + QLineEdit, ) -from PyQt5.QtCore import Qt +from PyQt6.QtCore import Qt from services.core.loading_service import run_with_progress from database.models.setting import Setting from .forms.setting_form import SettingForm PAGE_SIZE = 10 -# --- Settings Tab --- + class SettingsTab(QWidget): def __init__(self): super().__init__() @@ -24,18 +31,18 @@ class SettingsTab(QWidget): layout = QVBoxLayout() - # Top Layout + # --- Top layout (placeholder cho search/filter nếu cần) --- top_layout = QHBoxLayout() top_layout.addStretch() layout.addLayout(top_layout) - # Table + # --- Table --- self.table = QTableWidget() - self.table.setEditTriggers(QTableWidget.NoEditTriggers) - self.table.setSelectionBehavior(QTableWidget.SelectRows) + self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) layout.addWidget(self.table) - # Pagination + # --- Pagination --- pag_layout = QHBoxLayout() self.prev_btn = QPushButton("Previous") self.prev_btn.clicked.connect(self.prev_page) @@ -57,8 +64,9 @@ class SettingsTab(QWidget): self.setLayout(layout) - # --- Load Data --- + # ---------------------------------------------------------------------- def load_data(self, show_progress=True): + """Nạp dữ liệu setting vào bảng""" self.table.clearContents() self.table.setRowCount(0) @@ -82,12 +90,12 @@ class SettingsTab(QWidget): # ID id_item = QTableWidgetItem(str(setting_id)) - id_item.setFlags(id_item.flags() & ~Qt.ItemIsEditable) + id_item.setFlags(id_item.flags() & ~Qt.ItemFlag.ItemIsEditable) self.table.setItem(i_row, 0, id_item) # Key key_item = QTableWidgetItem(key) - key_item.setFlags(key_item.flags() & ~Qt.ItemIsEditable) + key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable) self.table.setItem(i_row, 1, key_item) # Value @@ -99,32 +107,42 @@ class SettingsTab(QWidget): btn_menu.setMaximumWidth(120) menu = QMenu(btn_menu) act_edit = menu.addAction("Edit") - act_edit.triggered.connect(partial(self.edit_setting, setting_id, key, i_row, type_)) + act_edit.triggered.connect( + partial(self.edit_setting, setting_id, key, i_row, type_) + ) btn_menu.setMenu(menu) self.table.setCellWidget(i_row, 3, btn_menu) items_with_index = [(s, i) for i, s in enumerate(settings)] if show_progress: - run_with_progress(items_with_index, handler=lambda x: handler(*x), message="Loading settings...", parent=self) + run_with_progress( + items_with_index, + handler=lambda x: handler(*x), + message="Loading settings...", + parent=self, + ) else: for item in items_with_index: handler(*item) - # Resize header + # --- Resize header --- header = self.table.horizontalHeader() - header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID - header.setSectionResizeMode(1, QHeaderView.Stretch) # Key - header.setSectionResizeMode(2, QHeaderView.Stretch) # Value - header.setSectionResizeMode(3, QHeaderView.Fixed) # Action + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # ID + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Key + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Value + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Action self.table.setColumnWidth(3, 120) - # Pagination + # --- Pagination --- self.prev_btn.setEnabled(self.current_page > 0) self.next_btn.setEnabled(self.current_page < self.total_pages - 1) - self.page_info_label.setText(f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)") + self.page_info_label.setText( + f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)" + ) - # --- Edit setting --- + # ---------------------------------------------------------------------- def edit_setting(self, setting_id, key, row, type_): + """Chỉnh sửa giá trị setting""" old_value = self.table.item(row, 2).text() new_value = SettingForm.get_new_value(key, old_value, type_=type_, parent=self) if new_value is not None and new_value != old_value: @@ -134,8 +152,9 @@ class SettingsTab(QWidget): except Exception as e: QMessageBox.critical(self, "Error", f"Failed to update setting: {e}") - # --- Pagination --- + # ---------------------------------------------------------------------- def go_to_page(self): + """Nhảy đến trang cụ thể""" try: page = int(self.page_input.text()) - 1 if 0 <= page < self.total_pages: @@ -145,11 +164,13 @@ class SettingsTab(QWidget): pass def next_page(self): + """Trang kế""" if self.current_page < self.total_pages - 1: self.current_page += 1 self.load_data() def prev_page(self): + """Trang trước""" if self.current_page > 0: self.current_page -= 1 self.load_data() diff --git a/requirements.txt b/requirements.txt index 8c8e0d0..3cc13a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ -PyQt5==5.15.11 -PyQtWebEngine==5.15.6 +PyQt6==6.7.0 +PyQt6-WebEngine==6.7.0 opencv-python==4.10.0.84 numpy==1.26.4 SQLAlchemy==2.0.22 requests - diff --git a/services/action_service.py b/services/action_service.py index 9dd4361..c68b66c 100644 --- a/services/action_service.py +++ b/services/action_service.py @@ -1,4 +1,4 @@ -from PyQt5.QtCore import Qt, QTimer +from PyQt6.QtCore import Qt, QTimer class ActionService: @@ -16,7 +16,7 @@ class ActionService: self.delay = delay # ---------------------------------------------------------------------- - def _run_js(self, script): + def _run_js(self, script: str): """Chạy JavaScript trên webview""" if not self.webview: print("[WARN] Không có webview để chạy JS.") diff --git a/services/core/loading_service.py b/services/core/loading_service.py index 783bc72..2bddc27 100644 --- a/services/core/loading_service.py +++ b/services/core/loading_service.py @@ -1,7 +1,10 @@ -from PyQt5.QtWidgets import QProgressDialog -from PyQt5.QtCore import Qt, QCoreApplication +from PyQt6.QtWidgets import QProgressDialog +from PyQt6.QtCore import Qt, QCoreApplication -def run_with_progress(items, handler, message="Loading...", cancel_text="Cancel", parent=None): + +def run_with_progress( + items, handler, message="Loading...", cancel_text="Cancel", parent=None +): """ Run the handler for each item in `items` and display a loading progress bar. - items: iterable (list, tuple, ...) @@ -15,7 +18,7 @@ def run_with_progress(items, handler, message="Loading...", cancel_text="Cancel" return 0, 0 progress = QProgressDialog(message, cancel_text, 0, total, parent) - progress.setWindowModality(Qt.WindowModal) + progress.setWindowModality(Qt.WindowModality.WindowModal) # PyQt6: enum thay đổi progress.setMinimumDuration(0) progress.setValue(0) @@ -33,7 +36,7 @@ def run_with_progress(items, handler, message="Loading...", cancel_text="Cancel" fail_count += 1 progress.setValue(i + 1) - QCoreApplication.processEvents() # Prevent UI freezing + QCoreApplication.processEvents() # Giữ UI không bị treo progress.close() return success_count, fail_count diff --git a/services/image_service.py b/services/image_service.py index 3a4b3a7..57eaa80 100644 --- a/services/image_service.py +++ b/services/image_service.py @@ -1,8 +1,9 @@ # services/image_service.py import os import requests -from PyQt5.QtGui import QPixmap -from PyQt5.QtCore import Qt +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt + class ImageService: @staticmethod @@ -35,7 +36,13 @@ class ImageService: return None if not pixmap.isNull(): - pixmap = pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + # PyQt6: enum cần gọi qua Qt.AspectRatioMode và Qt.TransformationMode + pixmap = pixmap.scaled( + size, + size, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) return pixmap return None diff --git a/services/profile_service.py b/services/profile_service.py index a305da1..31cf730 100644 --- a/services/profile_service.py +++ b/services/profile_service.py @@ -35,7 +35,7 @@ class ProfileService: Service để quản lý thư mục profiles. Mặc định root folder lấy từ config.PROFILES_DIR. """ - + base_dir = PROFILES_DIR def __init__(self, profiles_root: Optional[str] = None): @@ -47,7 +47,7 @@ class ProfileService: def get_profile_dirname(self, name: str) -> str: """Tên folder đã sanitize (chỉ tên folder, không có path)""" return _sanitize_name(name) - + def save_profile(self, key: str): # ở đây có thể không cần làm gì nhiều vì Qt tự lưu cookie # nhưng bạn có thể log hoặc thêm custom logic @@ -62,7 +62,9 @@ class ProfileService: """Check folder có tồn tại không""" return os.path.isdir(self.get_profile_path(name)) - def create(self, name: str, copy_from: Optional[str] = None, exist_ok: bool = True) -> str: + def create( + self, name: str, copy_from: Optional[str] = None, exist_ok: bool = True + ) -> str: """ Tạo folder profile. - name: email/username @@ -118,19 +120,21 @@ class ProfileService: return [] # ----------------- Optional: QWebEngineProfile creator ----------------- - def create_qwebengine_profile(self, name: str, parent=None, profile_id: Optional[str] = None): + def create_qwebengine_profile( + self, name: str, parent=None, profile_id: Optional[str] = None + ): """ Tạo và trả về QWebEngineProfile đã cấu hình persistent storage (cookies, local storage, cache). - Yêu cầu PyQt5.QtWebEngineWidgets được cài. + Yêu cầu PyQt6.QtWebEngineWidgets được cài. - name: email/username để đặt thư mục lưu - parent: parent QObject cho QWebEngineProfile (thường là self) - profile_id: tên id cho profile (tùy chọn) """ try: - from PyQt5.QtWebEngineWidgets import QWebEngineProfile + from PyQt6.QtWebEngineWidgets import QWebEngineProfile except Exception as e: raise RuntimeError( - "PyQt5.QtWebEngineWidgets không khả dụng. " + "PyQt6.QtWebEngineWidgets không khả dụng. " "Không thể tạo QWebEngineProfile." ) from e @@ -143,8 +147,13 @@ class ProfileService: profile.setCachePath(profile_path) # Force lưu cookie persist try: - profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies) + from PyQt6.QtWebEngineCore import QWebEngineProfile as CoreProfile + + profile.setPersistentCookiesPolicy( + CoreProfile.PersistentCookiesPolicy.ForcePersistentCookies + ) except Exception: pass + logger.info("Created QWebEngineProfile for %s -> %s", name, profile_path) return profile diff --git a/stores/shared_store.py b/stores/shared_store.py index fab2488..15dfd5f 100644 --- a/stores/shared_store.py +++ b/stores/shared_store.py @@ -1,6 +1,7 @@ -from PyQt5.QtCore import QObject +from PyQt6.QtCore import QObject import threading + class SharedStore(QObject): _instance = None _lock = threading.Lock() @@ -24,7 +25,7 @@ class SharedStore(QObject): def remove(self, listed_id: int): with self._items_lock: - self._items = [i for i in self._items if i["listed_id"] != listed_id] + self._items = [i for i in self._items if i.get("listed_id") != listed_id] def size(self) -> int: with self._items_lock: