upgrate PyQt6
This commit is contained in:
		
							parent
							
								
									1aba2bf683
								
							
						
					
					
						commit
						671fadf645
					
				
							
								
								
									
										6
									
								
								app.py
								
								
								
								
							
							
						
						
									
										6
									
								
								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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										18
									
								
								fb_window.py
								
								
								
								
							
							
						
						
									
										18
									
								
								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())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}"
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
 | 
			
		||||
# Tạo instance toàn cục để các module khác có thể import dùng chung
 | 
			
		||||
global_signals = GlobalSignals()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue