upgrate PyQt6

This commit is contained in:
nkhangg 2025-10-16 21:56:59 +07:00
parent 1aba2bf683
commit 671fadf645
24 changed files with 703 additions and 347 deletions

6
app.py
View File

@ -1,14 +1,16 @@
import sys import sys
from PyQt5.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from gui.main_window import MainWindow from gui.main_window import MainWindow
from database.db import create_tables from database.db import create_tables
def main(): def main():
create_tables() # tạo bảng nếu chưa tồn tại create_tables() # tạo bảng nếu chưa tồn tại
app = QApplication(sys.argv) app = QApplication(sys.argv)
window = MainWindow() window = MainWindow()
window.show() window.show()
sys.exit(app.exec_()) sys.exit(app.exec())
if __name__ == "__main__": if __name__ == "__main__":
main() main()

Binary file not shown.

View File

@ -2,8 +2,8 @@ import os
import sys import sys
import cv2 import cv2
import numpy as np import numpy as np
from PyQt5.QtCore import Qt, QUrl, QTimer from PyQt6.QtCore import Qt, QUrl, QTimer
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QApplication,
QMainWindow, QMainWindow,
QVBoxLayout, QVBoxLayout,
@ -12,8 +12,8 @@ from PyQt5.QtWidgets import (
QLabel, QLabel,
QDialog, QDialog,
) )
from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtGui import QPixmap, QImage from PyQt6.QtGui import QPixmap, QImage
from services.action_service import ActionService # ✅ JS version (fake click/type) 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.btn_detect.clicked.connect(self.detect_inputs_from_view)
self.status = QLabel("Status: Ready") self.status = QLabel("Status: Ready")
self.status.setAlignment(Qt.AlignLeft) self.status.setAlignment(Qt.AlignmentFlag.AlignLeft)
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(self.web) layout.addWidget(self.web)
@ -195,7 +195,7 @@ class FBWindow(QMainWindow):
# ---------------------------------------------------- # ----------------------------------------------------
def show_preview(self, bgr_img): 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) rgb = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB)
h, w = rgb.shape[:2] h, w = rgb.shape[:2]
qimg = QImage(rgb.data, w, h, rgb.strides[0], QImage.Format.Format_RGB888) qimg = QImage(rgb.data, w, h, rgb.strides[0], QImage.Format.Format_RGB888)
@ -205,13 +205,13 @@ class FBWindow(QMainWindow):
dlg.setWindowTitle("Detection Preview") dlg.setWindowTitle("Detection Preview")
v_layout = QVBoxLayout(dlg) v_layout = QVBoxLayout(dlg)
lbl = QLabel() lbl = QLabel()
lbl.setPixmap(pix.scaled(800, 600, Qt.KeepAspectRatio)) lbl.setPixmap(pix.scaled(800, 600, Qt.AspectRatioMode.KeepAspectRatio))
v_layout.addWidget(lbl) v_layout.addWidget(lbl)
dlg.exec_() dlg.exec()
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
win = FBWindow() win = FBWindow()
win.show() win.show()
sys.exit(app.exec_()) sys.exit(app.exec())

View File

@ -1,6 +1,6 @@
# gui/core/login_handle_dialog.py # gui/core/login_handle_dialog.py
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QPushButton, QApplication from PyQt6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QApplication
from PyQt5.QtCore import QTimer from PyQt6.QtCore import QTimer
from services.core.log_service import log_service from services.core.log_service import log_service
from stores.shared_store import SharedStore from stores.shared_store import SharedStore
from gui.global_signals import global_signals from gui.global_signals import global_signals
@ -11,7 +11,7 @@ class LoginHandleDialog(QDialog):
dialog_height = 100 dialog_height = 100
margin = 10 # khoảng cách giữa các dialog 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 = [] open_dialogs = []
def __init__(self, account_id: int, listed_id: int): def __init__(self, account_id: int, listed_id: int):
@ -30,11 +30,12 @@ class LoginHandleDialog(QDialog):
layout.addWidget(self.btn_finish) layout.addWidget(self.btn_finish)
self.setLayout(layout) 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() self.move_to_corner()
LoginHandleDialog.open_dialogs.append(self) LoginHandleDialog.open_dialogs.append(self)
def move_to_corner(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() screen_geometry = QApplication.primaryScreen().availableGeometry()
start_x = screen_geometry.left() + self.margin start_x = screen_geometry.left() + self.margin
start_y = screen_geometry.top() + self.margin start_y = screen_geometry.top() + self.margin
@ -66,15 +67,25 @@ class LoginHandleDialog(QDialog):
try: try:
store = SharedStore.get_instance() store = SharedStore.get_instance()
store.remove(self.listed_id) 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 # 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 # Close dialog an toàn
self.hide() self.hide()
self.deleteLater() self.deleteLater()
if self in LoginHandleDialog.open_dialogs: if self in LoginHandleDialog.open_dialogs:
LoginHandleDialog.open_dialogs.remove(self) LoginHandleDialog.open_dialogs.remove(self)
except Exception as e: 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}"
)

View File

@ -1,9 +1,12 @@
# gui/global_signals.py # gui/global_signals.py
from PyQt5.QtCore import QObject, pyqtSignal from PyQt6.QtCore import QObject, pyqtSignal
class GlobalSignals(QObject): class GlobalSignals(QObject):
listed_finished = pyqtSignal() listed_finished = pyqtSignal()
open_login_dialog = pyqtSignal(int, int) # account_id, listed_id 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
# Tạo instance toàn cục để các module khác có thể import dùng chung
global_signals = GlobalSignals() global_signals = GlobalSignals()

View File

@ -2,10 +2,19 @@ import os
import sys import sys
import cv2 import cv2
import numpy as np import numpy as np
from PyQt5.QtCore import Qt, QUrl, QTimer from PyQt6.QtCore import Qt, QUrl, QTimer
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QTextEdit, QPushButton from PyQt6.QtWidgets import (
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile QApplication,
from PyQt5.QtGui import QImage 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.action_service import ActionService
from services.detect_service import DetectService from services.detect_service import DetectService
@ -14,41 +23,47 @@ from config import TEMPLATE_DIR
class LoginFB(QMainWindow): 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): def __init__(self, account=None, delay=0.3):
super().__init__() super().__init__()
self.account = account or {} self.account = account or {}
self.template_dir = os.path.abspath(TEMPLATE_DIR)
self.delay = delay self.delay = delay
self.template_dir = os.path.abspath(TEMPLATE_DIR)
# ✅ Lấy tên profile từ email hoặc username # ✅ 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 --- # --- Detect services ---
self.detector = DetectService( self.detector = DetectService(
template_dir=TEMPLATE_DIR, template_dir=TEMPLATE_DIR, target_labels=["username", "password"]
target_labels=["username", "password"]
) )
# --- UI cơ bản --- # --- UI setup ---
self.setWindowTitle(f"FB Auto Vision Login - {self.profile_name}") self.setWindowTitle(f"FB Auto Vision Login - {self.profile_name}")
self.setFixedSize(480, 680) self.setFixedSize(480, 680)
self.web = QWebEngineView() self.web = QWebEngineView()
self.status = QLabel("Status: Ready") self.status = QLabel("Status: Ready")
self.status.setAlignment(Qt.AlignLeft) self.status.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.status.setFixedHeight(20) self.status.setFixedHeight(20)
# Log area
self.log_area = QTextEdit() self.log_area = QTextEdit()
self.log_area.setReadOnly(True) self.log_area.setReadOnly(True)
self.log_area.setFixedHeight(120) self.log_area.setFixedHeight(120)
self.log_area.setStyleSheet(""" self.log_area.setStyleSheet(
"""
background-color: #1e1e1e; background-color: #1e1e1e;
color: #dcdcdc; color: #dcdcdc;
font-size: 12px; font-size: 12px;
font-family: Consolas, monospace; font-family: Consolas, monospace;
""") """
)
# Refresh button
self.btn_refresh = QPushButton("Refresh") self.btn_refresh = QPushButton("Refresh")
self.btn_refresh.setFixedHeight(30) self.btn_refresh.setFixedHeight(30)
self.btn_refresh.clicked.connect(self.refresh_page) self.btn_refresh.clicked.connect(self.refresh_page)
@ -56,9 +71,14 @@ class LoginFB(QMainWindow):
# --- Profile --- # --- Profile ---
self.profile_service = ProfileService() self.profile_service = ProfileService()
profile_path = self.profile_service.get_profile_path(self.profile_name) profile_path = self.profile_service.get_profile_path(self.profile_name)
os.makedirs(profile_path, exist_ok=True)
profile = self.web.page().profile() profile = self.web.page().profile()
profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies) profile.setPersistentCookiesPolicy(
QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies
)
profile.setPersistentStoragePath(profile_path) profile.setPersistentStoragePath(profile_path)
self.log(f"[INFO] Profile applied at: {profile_path}") self.log(f"[INFO] Profile applied at: {profile_path}")
# --- Webview --- # --- Webview ---
@ -83,15 +103,18 @@ class LoginFB(QMainWindow):
# ---------------------------------------------------- # ----------------------------------------------------
def log(self, message: str): def log(self, message: str):
"""Ghi log vào vùng log và console."""
self.log_area.append(message) self.log_area.append(message)
print(message) print(message)
# ---------------------------------------------------- # ----------------------------------------------------
def capture_webview(self): def capture_webview(self):
"""Chụp hình ảnh nội dung webview dưới dạng numpy array (BGR)."""
pixmap = self.web.grab() pixmap = self.web.grab()
if pixmap.isNull(): if pixmap.isNull():
return None return None
qimg = pixmap.toImage().convertToFormat(QImage.Format_RGBA8888)
qimg = pixmap.toImage().convertToFormat(QImage.Format.Format_RGBA8888)
width, height = qimg.width(), qimg.height() width, height = qimg.width(), qimg.height()
ptr = qimg.bits() ptr = qimg.bits()
ptr.setsize(height * width * 4) ptr.setsize(height * width * 4)
@ -100,12 +123,13 @@ class LoginFB(QMainWindow):
# ---------------------------------------------------- # ----------------------------------------------------
def on_web_loaded(self, ok=True): def on_web_loaded(self, ok=True):
"""Khi trang web load xong."""
if not ok: if not ok:
self.log("[ERROR] Page failed to load") self.log("[ERROR] Page failed to load")
self.status.setText("Status: Page load failed") self.status.setText("Status: Page load failed")
return return
self.log("[INFO] Page loaded") self.log("[INFO] Page loaded successfully")
self.status.setText("Status: Page loaded") self.status.setText("Status: Page loaded")
# ✅ Lưu profile khi load xong # ✅ Lưu profile khi load xong
@ -116,7 +140,7 @@ class LoginFB(QMainWindow):
screen = self.capture_webview() screen = self.capture_webview()
if screen is None: if screen is None:
self.status.setText("Status: Unable to capture webview") self.status.setText("Status: Unable to capture webview")
self.log("Status: Unable to capture webview") self.log("[WARN] Unable to capture webview")
return return
self.log("[INFO] Detecting email/password fields...") 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.status.setText(f"[INFO] Detected {len(regions)} valid regions")
self.log(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)) QTimer.singleShot(500, lambda: self.autofill_by_detection(regions))
# ---------------------------------------------------- # ----------------------------------------------------
def autofill_by_detection(self, 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", "") email = self.account.get("email", "")
password = self.account.get("password", "") password = self.account.get("password", "")
# sắp xếp để điền username trước, password sau # 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())) ordered = sorted(
regions, key=lambda r: ("pass" in r[0].lower(), "user" not in r[0].lower())
)
def do_action(i=0): def do_action(i=0):
if i >= len(ordered): if i >= len(ordered):
return return
folder_name, filename, top_left, bottom_right, score = ordered[i] folder_name, filename, top_left, bottom_right, score = ordered[i]
label = folder_name.lower() label = folder_name.lower()
@ -161,13 +191,15 @@ class LoginFB(QMainWindow):
# ---------------------------------------------------- # ----------------------------------------------------
def refresh_page(self): def refresh_page(self):
"""Tải lại trang."""
self.log("[INFO] Refreshing page...") self.log("[INFO] Refreshing page...")
self.web.reload() self.web.reload()
# ----------------------------------------------------
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
fake_account = {"email": "test@example.com", "password": "123456"} fake_account = {"email": "test@example.com", "password": "123456"}
win = LoginFB(account=fake_account, delay=0.5) win = LoginFB(account=fake_account, delay=0.5)
win.show() win.show()
sys.exit(app.exec_()) sys.exit(app.exec())

View File

@ -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.accounts.account_tab import AccountTab
from gui.tabs.products.product_tab import ProductTab from gui.tabs.products.product_tab import ProductTab
from gui.tabs.import_tab import ImportTab from gui.tabs.import_tab import ImportTab
@ -34,51 +34,62 @@ class MainWindow(QMainWindow):
self.setCentralWidget(self.tabs) self.setCentralWidget(self.tabs)
self.on_tab_changed(0) self.on_tab_changed(0)
# # --- Signals --- # --- Signals ---
# global_signals.listed_finished.connect(self.on_listed_task_finished) # global_signals.listed_finished.connect(self.on_listed_task_finished)
# global_signals.dialog_finished.connect(self.on_dialog_finished) # global_signals.dialog_finished.connect(self.on_dialog_finished)
# global_signals.open_login_dialog.connect(self.show_login_dialog) # global_signals.open_login_dialog.connect(self.show_login_dialog)
# # --- Start background --- # --- Background tasks ---
# start_background_listed() # start_background_listed()
# --- Store opened dialogs --- # --- Store opened dialogs ---
self.login_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 = 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) self.login_dialogs.append(dialog)
# Khi dialog đóng, remove khỏi list và SharedStore # Cleanup khi dialog đóng
def on_dialog_close(): dialog.finished.connect(
if dialog in self.login_dialogs: lambda: self._on_dialog_closed(dialog, account_id, listed_id)
self.login_dialogs.remove(dialog) )
self.on_dialog_finished(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): 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 = SharedStore.get_instance()
store.remove(listed_id) 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) tab = self.tabs.widget(index)
if hasattr(tab, "load_data") and not getattr(tab, "is_loaded", False): if hasattr(tab, "load_data") and not getattr(tab, "is_loaded", False):
tab.load_data() tab.load_data()
tab.is_loaded = True tab.is_loaded = True
# ----------------------------------------------------------------------
def on_listed_task_finished(self): def on_listed_task_finished(self):
"""Khi task nền hoàn tất"""
QMessageBox.information( QMessageBox.information(
self, self, "Auto Listing", "All pending listed items have been processed!"
"Auto Listing",
"All pending listed items have been processed!"
) )
# ----------------------------------------------------------------------
def closeEvent(self, event): def closeEvent(self, event):
"""Khi MainWindow đóng, đóng hết tất cả dialog đang mở""" """Khi đóng MainWindow -> đóng hết dialog con"""
for dialog in self.login_dialogs[:]: # copy list để tránh lỗi khi remove for dialog in self.login_dialogs[:]: # copy để tránh modify khi lặp
dialog.close() dialog.close()
event.accept() event.accept()

View File

@ -1,22 +1,30 @@
import os import os
from functools import partial from functools import partial
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QWidget,
QPushButton, QHBoxLayout, QDialog, QLabel, QLineEdit, QComboBox, QMessageBox, QVBoxLayout,
QMenu, QAction, QSizePolicy QTableWidget,
QTableWidgetItem,
QPushButton,
QHBoxLayout,
QMessageBox,
QMenu,
QSizePolicy,
QHeaderView,
) )
from PyQt5.QtCore import Qt from PyQt6.QtGui import QAction, QFont
from PyQt5.QtGui import QFont from PyQt6.QtCore import Qt
from PyQt5.QtWidgets import QHeaderView from PyQt6.QtGui import QFont
from database.models import Account from database.models import Account
from .forms.account_form import AccountForm from .forms.account_form import AccountForm
from config import PROFILES_DIR from config import PROFILES_DIR
# 👇 import cửa sổ login FB # 👇 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 PAGE_SIZE = 10
class AccountTab(QWidget): class AccountTab(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -31,7 +39,9 @@ class AccountTab(QWidget):
# 🆕 Action menu # 🆕 Action menu
self.options_btn = QPushButton("Action") 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.setMinimumWidth(80)
self.options_btn.setMaximumWidth(120) self.options_btn.setMaximumWidth(120)
top_layout.addWidget(self.options_btn) top_layout.addWidget(self.options_btn)
@ -40,7 +50,9 @@ class AccountTab(QWidget):
# Table # Table
self.table = QTableWidget() self.table = QTableWidget()
self.table.verticalHeader().setDefaultSectionSize(28) # row gọn 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) layout.addWidget(self.table)
# Pagination # Pagination
@ -68,16 +80,22 @@ class AccountTab(QWidget):
self.table.setRowCount(len(page_items)) self.table.setRowCount(len(page_items))
self.table.setColumnCount(6) self.table.setColumnCount(6)
self.table.setHorizontalHeaderLabels([ self.table.setHorizontalHeaderLabels(
"ID", "Email", "Status", "Profile Exists", "Login At", "Actions" ["ID", "Email", "Status", "Profile Exists", "Login At", "Actions"]
]) )
for i, acc in enumerate(page_items): for i, acc in enumerate(page_items):
acc_dict = {k: acc[k] for k in acc.keys()} acc_dict = {k: acc[k] for k in acc.keys()}
self.table.setItem(i, 0, QTableWidgetItem(str(acc_dict["id"]))) self.table.setItem(i, 0, QTableWidgetItem(str(acc_dict["id"])))
self.table.setItem(i, 1, QTableWidgetItem(acc_dict["email"])) 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 # ✅ Check profile folder
folder_name = acc_dict["email"] folder_name = acc_dict["email"]
@ -95,7 +113,9 @@ class AccountTab(QWidget):
# 🆕 Login action # 🆕 Login action
action_login = QAction("Login", btn_menu) 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) menu.addAction(action_login)
# Edit # Edit
@ -105,7 +125,9 @@ class AccountTab(QWidget):
# Delete # Delete
action_delete = QAction("Delete", btn_menu) 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) menu.addAction(action_delete)
btn_menu.setMenu(menu) btn_menu.setMenu(menu)
@ -113,12 +135,12 @@ class AccountTab(QWidget):
# Column sizing # Column sizing
header = self.table.horizontalHeader() header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(2, QHeaderView.ResizeToContents) header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(3, QHeaderView.ResizeToContents) header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(4, QHeaderView.ResizeToContents) header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(5, QHeaderView.Fixed) header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed)
self.table.setColumnWidth(5, 100) self.table.setColumnWidth(5, 100)
self.prev_btn.setEnabled(self.current_page > 0) self.prev_btn.setEnabled(self.current_page > 0)
@ -134,21 +156,23 @@ class AccountTab(QWidget):
def add_account(self): def add_account(self):
form = AccountForm(self) form = AccountForm(self)
if form.exec_(): if form.exec():
self.current_page = 0 self.current_page = 0
self.load_data() self.load_data()
def edit_account(self, account): def edit_account(self, account):
form = AccountForm(self, account) form = AccountForm(self, account)
if form.exec_(): if form.exec():
self.load_data() self.load_data()
def delete_account(self, account): def delete_account(self, account):
confirm = QMessageBox.question( confirm = QMessageBox.question(
self, "Confirm", f"Delete account {account['email']}?", self,
QMessageBox.Yes | QMessageBox.No "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"]) Account.delete(account["id"])
self.load_data() self.load_data()
@ -162,5 +186,7 @@ class AccountTab(QWidget):
# 🆕 Mở cửa sổ login FB # 🆕 Mở cửa sổ login FB
def open_login_window(self, account): 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() self.login_window.show()

View File

@ -1,9 +1,18 @@
from PyQt5.QtWidgets import ( # gui/dialogs/account_form.py
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QComboBox, from PyQt6.QtWidgets import (
QPushButton, QMessageBox QDialog,
QVBoxLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QComboBox,
QPushButton,
QMessageBox,
) )
from PyQt6.QtCore import Qt
from database.models.account import Account from database.models.account import Account
class AccountForm(QDialog): class AccountForm(QDialog):
def __init__(self, parent=None, account=None): def __init__(self, parent=None, account=None):
super().__init__(parent) super().__init__(parent)
@ -23,7 +32,7 @@ class AccountForm(QDialog):
layout.addWidget(QLabel("Password")) layout.addWidget(QLabel("Password"))
pw_layout = QHBoxLayout() pw_layout = QHBoxLayout()
self.password_input = QLineEdit() self.password_input = QLineEdit()
self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
self.password_input.setMinimumWidth(200) self.password_input.setMinimumWidth(200)
self.toggle_btn = QPushButton("Show") self.toggle_btn = QPushButton("Show")
@ -50,7 +59,7 @@ class AccountForm(QDialog):
self.cancel_btn = QPushButton("Cancel") self.cancel_btn = QPushButton("Cancel")
self.cancel_btn.setMinimumWidth(80) 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) btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout) layout.addLayout(btn_layout)
@ -60,21 +69,29 @@ class AccountForm(QDialog):
if account: if account:
self.email_input.setText(account.get("email", "")) self.email_input.setText(account.get("email", ""))
self.password_input.setText(account.get("password", "")) 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): def toggle_password(self):
"""Ẩn/hiện password khi nhấn nút""" """Ẩn/hiện password khi nhấn nút"""
if self.toggle_btn.isChecked(): if self.toggle_btn.isChecked():
self.password_input.setEchoMode(QLineEdit.Normal) self.password_input.setEchoMode(QLineEdit.EchoMode.Normal)
self.toggle_btn.setText("Hide") self.toggle_btn.setText("Hide")
else: else:
self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
self.toggle_btn.setText("Show") self.toggle_btn.setText("Show")
def save(self): def save(self):
email = self.email_input.text() """Lưu hoặc cập nhật account"""
password = self.password_input.text() email = self.email_input.text().strip()
password = self.password_input.text().strip()
is_active = 1 if self.active_input.currentText() == "Active" else 0 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: try:
if self.account and "id" in self.account: if self.account and "id" in self.account:
Account.update(self.account["id"], email, password, is_active) Account.update(self.account["id"], email, password, is_active)
@ -82,4 +99,4 @@ class AccountForm(QDialog):
Account.create(email, password, is_active) Account.create(email, password, is_active)
self.accept() self.accept()
except Exception as e: except Exception as e:
QMessageBox.warning(self, "Error", str(e)) QMessageBox.critical(self, "Error", f"Failed to save account:\n{e}")

View File

@ -3,9 +3,15 @@ import time
from services.core.loading_service import run_with_progress from services.core.loading_service import run_with_progress
from database.models.product import Product from database.models.product import Product
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QPushButton, QFileDialog, QWidget,
QTableWidget, QTableWidgetItem, QHBoxLayout, QMessageBox QVBoxLayout,
QPushButton,
QFileDialog,
QTableWidget,
QTableWidgetItem,
QHBoxLayout,
QMessageBox,
) )
@ -40,13 +46,15 @@ class ImportTab(QWidget):
self.preview_data = [] # store imported data self.preview_data = [] # store imported data
def import_csv(self): 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: if not file_path:
return return
try: try:
# đọc CSV chuẩn với DictReader # đọ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) reader = csv.DictReader(csvfile)
headers = reader.fieldnames headers = reader.fieldnames
rows = list(reader) rows = list(reader)
@ -75,7 +83,9 @@ class ImportTab(QWidget):
QMessageBox.critical(self, "Error", f"Failed to read CSV: {e}") QMessageBox.critical(self, "Error", f"Failed to read CSV: {e}")
def import_api(self): 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): def save_to_db(self):
if not self.preview_data: if not self.preview_data:
@ -87,14 +97,13 @@ class ImportTab(QWidget):
self, self,
"Confirm Import", "Confirm Import",
f"Are you sure you want to import {len(self.preview_data)} rows?", 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 return
def handler(item): def handler(item):
try: try:
# time.sleep(0.05) # có thể bỏ nếu không cần debug progress
Product.insert_from_import(item) Product.insert_from_import(item)
return True return True
except Exception as e: except Exception as e:
@ -102,16 +111,13 @@ class ImportTab(QWidget):
return False return False
success, fail = run_with_progress( success, fail = run_with_progress(
self.preview_data, self.preview_data, handler=handler, message="Importing data...", parent=self
handler=handler,
message="Importing data...",
parent=self
) )
QMessageBox.information( QMessageBox.information(
self, self,
"Import Completed", "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 # ✅ Clear preview sau khi import xong

View File

@ -1,10 +1,16 @@
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QFormLayout, QLineEdit, QDialogButtonBox, QDialog,
QVBoxLayout, QWidget, QComboBox QFormLayout,
QLineEdit,
QDialogButtonBox,
QVBoxLayout,
QWidget,
QComboBox,
) )
class ListedFilterDialog(QDialog): 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): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -27,15 +33,17 @@ class ListedFilterDialog(QDialog):
self.account_input = QLineEdit() self.account_input = QLineEdit()
form_layout.addRow("Account Email:", self.account_input) form_layout.addRow("Account Email:", self.account_input)
# Status (dropdown) # Status
self.status_input = QComboBox() self.status_input = QComboBox()
self.status_input.addItems(self.STATUS_OPTIONS) self.status_input.addItems(self.STATUS_OPTIONS)
form_layout.addRow("Status:", self.status_input) form_layout.addRow("Status:", self.status_input)
main_layout.addWidget(form_widget) main_layout.addWidget(form_widget)
# Buttons # Buttons (PyQt6 khác cú pháp một chút)
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.accepted.connect(self.accept)
self.btn_box.rejected.connect(self.reject) self.btn_box.rejected.connect(self.reject)
main_layout.addWidget(self.btn_box) main_layout.addWidget(self.btn_box)

View File

@ -1,12 +1,27 @@
# gui/tabs/listeds/listed_tab.py
from functools import partial from functools import partial
import json import json
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QWidget,
QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView, QVBoxLayout,
QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton, QTableWidget,
QStyle, QStylePainter, QLineEdit, QProgressBar 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.core.loading_service import run_with_progress
from services.image_service import ImageService from services.image_service import ImageService
from database.models.listed import Listed from database.models.listed import Listed
@ -15,6 +30,7 @@ from gui.tabs.listeds.dialogs.listed_filter_dialog import ListedFilterDialog
PAGE_SIZE = 10 PAGE_SIZE = 10
# --- Header Checkbox --- # --- Header Checkbox ---
class CheckBoxHeader(QHeaderView): class CheckBoxHeader(QHeaderView):
select_all_changed = pyqtSignal(bool) select_all_changed = pyqtSignal(bool)
@ -33,9 +49,11 @@ class CheckBoxHeader(QHeaderView):
x = rect.x() + (rect.width() - size) // 2 x = rect.x() + (rect.width() - size) // 2
y = rect.y() + (rect.height() - size) // 2 y = rect.y() + (rect.height() - size) // 2
option.rect = QRect(x, y, size, size) 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 = QStylePainter(self.viewport())
painter2.drawControl(QStyle.CE_CheckBox, option) painter2.drawControl(QStyle.ControlElement.CE_CheckBox, option)
def handle_section_pressed(self, logicalIndex): def handle_section_pressed(self, logicalIndex):
if logicalIndex == 0: if logicalIndex == 0:
@ -46,11 +64,7 @@ class CheckBoxHeader(QHeaderView):
# --- ListedTab --- # --- ListedTab ---
class ListedTab(QWidget): class ListedTab(QWidget):
SORTABLE_COLUMNS = { SORTABLE_COLUMNS = {1: "id", 3: "product_name", 5: "listed_at"}
1: "id",
3: "product_name",
5: "listed_at"
}
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -63,43 +77,47 @@ class ListedTab(QWidget):
layout = QVBoxLayout() layout = QVBoxLayout()
# Top menu # --- Top layout ---
top_layout = QHBoxLayout() top_layout = QHBoxLayout()
top_layout.addStretch() # placeholder stretch top_layout.addStretch()
# --- Progress bar --- # Progress bar
self.progress_bar = QProgressBar() self.progress_bar = QProgressBar()
self.progress_bar.setMinimum(0) self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(100) self.progress_bar.setMaximum(100)
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
self.progress_bar.setTextVisible(True) self.progress_bar.setTextVisible(True)
self.progress_bar.setMinimumWidth(150) self.progress_bar.setMinimumWidth(150)
self.progress_bar.setMinimumHeight(25) # thêm chiều cao đủ lớn self.progress_bar.setMinimumHeight(25)
self.progress_bar.setAlignment(Qt.AlignCenter) # căn giữa text self.progress_bar.setAlignment(Qt.AlignmentFlag.AlignCenter)
top_layout.insertWidget(0, self.progress_bar) 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 = 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.setMinimumWidth(50)
self.options_btn.setMaximumWidth(120) self.options_btn.setMaximumWidth(120)
top_layout.addWidget(self.options_btn) top_layout.addWidget(self.options_btn)
layout.addLayout(top_layout) layout.addLayout(top_layout)
# Table # --- Table ---
self.table = QTableWidget() self.table = QTableWidget()
self.table.verticalHeader().setDefaultSectionSize(60) self.table.verticalHeader().setDefaultSectionSize(60)
self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
header = CheckBoxHeader(Qt.Horizontal, self.table)
header = CheckBoxHeader(Qt.Orientation.Horizontal, self.table)
self.table.setHorizontalHeader(header) self.table.setHorizontalHeader(header)
header.select_all_changed.connect(self.select_all_rows) header.select_all_changed.connect(self.select_all_rows)
header.sectionClicked.connect(self.handle_header_click) header.sectionClicked.connect(self.handle_header_click)
layout.addWidget(self.table) layout.addWidget(self.table)
# Pagination # --- Pagination ---
pag_layout = QHBoxLayout() pag_layout = QHBoxLayout()
self.prev_btn = QPushButton("Previous") self.prev_btn = QPushButton("Previous")
self.prev_btn.clicked.connect(self.prev_page) self.prev_btn.clicked.connect(self.prev_page)
@ -117,14 +135,14 @@ class ListedTab(QWidget):
self.next_btn = QPushButton("Next") self.next_btn = QPushButton("Next")
self.next_btn.clicked.connect(self.next_page) self.next_btn.clicked.connect(self.next_page)
pag_layout.addWidget(self.next_btn) pag_layout.addWidget(self.next_btn)
layout.addLayout(pag_layout)
layout.addLayout(pag_layout)
self.setLayout(layout) self.setLayout(layout)
# --- Timer để update progress --- # --- Timer ---
self.listing_timer = QTimer() self.listing_timer = QTimer()
self.listing_timer.timeout.connect(self.update_listed_progress) 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 --- # --- Load Data ---
def load_data(self, show_progress=True): def load_data(self, show_progress=True):
@ -133,20 +151,34 @@ class ListedTab(QWidget):
offset = self.current_page * PAGE_SIZE offset = self.current_page * PAGE_SIZE
page_items, total_count = Listed.get_paginated( page_items, total_count = Listed.get_paginated(
offset, PAGE_SIZE, self.filters, offset,
sort_by=self.sort_by, sort_order=self.sort_order PAGE_SIZE,
self.filters,
sort_by=self.sort_by,
sort_order=self.sort_order,
) )
self.total_count = total_count self.total_count = total_count
self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE) self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE)
self.table.setColumnCount(9) 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.setHorizontalHeaderLabels(columns)
self.table.setRowCount(len(page_items)) self.table.setRowCount(len(page_items))
def handler(item, i_row): def handler(item, i_row):
listed_id = item.get("id") listed_id = item.get("id")
# Checkbox # Checkbox
cb = QCheckBox() cb = QCheckBox()
cb.setProperty("listed_id", listed_id) cb.setProperty("listed_id", listed_id)
@ -160,32 +192,41 @@ class ListedTab(QWidget):
if pixmap: if pixmap:
lbl = QLabel() lbl = QLabel()
lbl.setPixmap(pixmap) lbl.setPixmap(pixmap)
lbl.setAlignment(Qt.AlignCenter) lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.table.setCellWidget(i_row, 1, lbl) self.table.setCellWidget(i_row, 1, lbl)
else: else:
self.table.setItem(i_row, 1, QTableWidgetItem("None")) self.table.setItem(i_row, 1, QTableWidgetItem("None"))
else: else:
self.table.setItem(i_row, 1, QTableWidgetItem("None")) 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, 2, QTableWidgetItem(item.get("sku") or ""))
self.table.setItem(i_row, 3, QTableWidgetItem(item.get("product_name") or "")) self.table.setItem(
self.table.setItem(i_row, 4, QTableWidgetItem(item.get("account_email") or "")) i_row, 3, QTableWidgetItem(item.get("product_name") or "")
)
self.table.setItem(
i_row, 4, QTableWidgetItem(item.get("account_email") or "")
)
# Listed At # Listed At
listed_str = "" listed_str = ""
ts = item.get("listed_at") ts = item.get("listed_at")
if ts: if ts:
from datetime import datetime from datetime import datetime
try: 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: except Exception:
listed_str = str(ts) listed_str = str(ts)
self.table.setItem(i_row, 5, QTableWidgetItem(listed_str)) self.table.setItem(i_row, 5, QTableWidgetItem(listed_str))
# Condition, Status # Condition, Status
self.table.setItem(i_row, 6, QTableWidgetItem(item.get("condition") or "")) 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 # Actions
btn_menu = QPushButton("Actions") btn_menu = QPushButton("Actions")
@ -198,25 +239,32 @@ class ListedTab(QWidget):
items_with_index = [(p, i) for i, p in enumerate(page_items)] items_with_index = [(p, i) for i, p in enumerate(page_items)]
if show_progress: 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: else:
for item in items_with_index: for item in items_with_index:
handler(*item) handler(*item)
# Header sizing # Header resize
header = self.table.horizontalHeader() header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.Fixed) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
self.table.setColumnWidth(1, 60) self.table.setColumnWidth(1, 60)
for idx in range(2, 8): for idx in range(2, 8):
header.setSectionResizeMode(idx, QHeaderView.Stretch) header.setSectionResizeMode(idx, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(8, QHeaderView.Fixed) header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed)
self.table.setColumnWidth(8, 100) self.table.setColumnWidth(8, 100)
# Pagination # Pagination info
self.prev_btn.setEnabled(self.current_page > 0) self.prev_btn.setEnabled(self.current_page > 0)
self.next_btn.setEnabled(self.current_page < self.total_pages - 1) 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 # Reset header checkbox
if isinstance(header, CheckBoxHeader): if isinstance(header, CheckBoxHeader):
@ -247,39 +295,59 @@ class ListedTab(QWidget):
menu.addAction(action_clear) menu.addAction(action_clear)
# Toggle Auto Listing # 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: if auto_setting:
setting_id = auto_setting["id"] setting_id = auto_setting.get("id")
auto_listing_val = auto_setting["value"].lower() == "true" auto_listing_val = str(auto_setting.get("value", "")).lower() == "true"
else: else:
auto_setting = Setting.create(Setting.AUTO_LISTING, "false") # create default if missing
setting_id = auto_setting["id"] try:
auto_listing_val = False 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( action_toggle_auto = QAction(
"Turn Auto Listing OFF" if auto_listing_val else "Turn Auto Listing ON", "Turn Auto Listing OFF" if auto_listing_val else "Turn Auto Listing ON",
menu menu,
) )
def toggle_auto_listing(): 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" new_val = "false" if auto_listing_val else "true"
try: try:
Setting.update_value(setting_id, new_val) Setting.update_value(setting_id, new_val)
QMessageBox.information( QMessageBox.information(
self, "Auto Listing", self, "Auto Listing", f"AUTO_LISTING set to {new_val.upper()}"
f"AUTO_LISTING set to {new_val.upper()}"
) )
# refresh menu and progress
self.update_options_menu() self.update_options_menu()
self.update_listed_progress() self.update_listed_progress()
except Exception as e: 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) action_toggle_auto.triggered.connect(toggle_auto_listing)
menu.addAction(action_toggle_auto) menu.addAction(action_toggle_auto)
# Delete Selected # Delete Selected
if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked() if any(
for i in range(self.table.rowCount())): 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 = QAction("Delete Selected", menu)
action_delete_selected.triggered.connect(self.delete_selected) action_delete_selected.triggered.connect(self.delete_selected)
menu.addAction(action_delete_selected) menu.addAction(action_delete_selected)
@ -287,7 +355,9 @@ class ListedTab(QWidget):
self.options_btn.setMenu(menu) self.options_btn.setMenu(menu)
def update_listed_progress(self): 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) self.progress_bar.setVisible(auto_listing_val)
if not auto_listing_val: if not auto_listing_val:
return return
@ -311,11 +381,10 @@ class ListedTab(QWidget):
self.progress_bar.setTextVisible(True) self.progress_bar.setTextVisible(True)
self.progress_bar.setFormat("0/0 listed") self.progress_bar.setFormat("0/0 listed")
# --- Filter --- # --- Filter ---
def open_filter_dialog(self): def open_filter_dialog(self):
dialog = ListedFilterDialog(self) dialog = ListedFilterDialog(self)
if dialog.exec_(): if dialog.exec(): # PyQt6: exec()
self.filters = dialog.get_filters() self.filters = dialog.get_filters()
self.current_page = 0 self.current_page = 0
self.load_data() self.load_data()
@ -366,29 +435,48 @@ class ListedTab(QWidget):
# --- Delete --- # --- Delete ---
def delete_selected(self): def delete_selected(self):
ids = [int(cb.property("listed_id")) for i in range(self.table.rowCount()) ids = [
if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked()] 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: if not ids:
QMessageBox.information(self, "Info", "No listed selected") QMessageBox.information(self, "Info", "No listed selected")
return return
confirm = QMessageBox.question( confirm = QMessageBox.question(
self, "Confirm Delete", f"Delete {len(ids)} selected listed items?", self,
QMessageBox.Yes | QMessageBox.No "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 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.current_page = 0
self.load_data() self.load_data()
def delete_listed(self, listed_id): def delete_listed(self, listed_id):
confirm = QMessageBox.question( 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 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() self.load_data()

View File

@ -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 services.core.loading_service import run_with_progress
from database.models.listed import Listed from database.models.listed import Listed
from database.models.account import Account # import model from database.models.account import Account # import model
class AddListedDialog(QDialog): class AddListedDialog(QDialog):
def __init__(self, product_ids, parent=None): def __init__(self, product_ids, parent=None):
super().__init__(parent) super().__init__(parent)
@ -27,6 +28,7 @@ class AddListedDialog(QDialog):
btn_ok = QPushButton("Add Listed") btn_ok = QPushButton("Add Listed")
btn_ok.clicked.connect(self.process_add_listed) btn_ok.clicked.connect(self.process_add_listed)
layout.addWidget(btn_ok) layout.addWidget(btn_ok)
self.setLayout(layout) self.setLayout(layout)
def process_add_listed(self): def process_add_listed(self):
@ -40,16 +42,20 @@ class AddListedDialog(QDialog):
return return
confirm = QMessageBox.question( confirm = QMessageBox.question(
self, "Confirm Add Listed", self,
"Confirm Add Listed",
f"Add {len(self.product_ids)} product(s) to listed under selected account?", 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 return
def handler(product_id): def handler(product_id):
try: 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: except Exception as e:
print(f"Error adding listed for product {product_id}: {e}") print(f"Error adding listed for product {product_id}: {e}")
@ -57,8 +63,10 @@ class AddListedDialog(QDialog):
self.product_ids, self.product_ids,
handler=handler, handler=handler,
message="Adding listed...", 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() self.accept()

View File

@ -1,9 +1,16 @@
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QFormLayout, QLineEdit, QDateEdit, QComboBox, QDialog,
QDialogButtonBox, QPushButton, QHBoxLayout, QVBoxLayout, QWidget QFormLayout,
QLineEdit,
QDateEdit,
QComboBox,
QDialogButtonBox,
QHBoxLayout,
QVBoxLayout,
QWidget,
) )
from PyQt5.QtCore import QDate from PyQt6.QtCore import QDate
import json
class FilterDialog(QDialog): class FilterDialog(QDialog):
SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"] SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"]
@ -62,11 +69,12 @@ class FilterDialog(QDialog):
right_form.addRow("Location:", self.location_input) right_form.addRow("Location:", self.location_input)
columns_layout.addWidget(right_widget) columns_layout.addWidget(right_widget)
main_layout.addLayout(columns_layout) main_layout.addLayout(columns_layout)
# --- Button Box --- # --- 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.accepted.connect(self.accept)
self.btn_box.rejected.connect(self.reject) self.btn_box.rejected.connect(self.reject)
main_layout.addWidget(self.btn_box) main_layout.addWidget(self.btn_box)

View File

@ -1,17 +1,30 @@
import json import json
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QDialog,
QComboBox, QTextEdit, QPushButton, QGridLayout, QVBoxLayout,
QFileDialog, QMessageBox 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 from database.models.product import Product
class ProductForm(QDialog): class ProductForm(QDialog):
SAMPLE_NAMES = [ SAMPLE_NAMES = [
"Apple iPhone 15", "Samsung Galaxy S23", "Sony Headphones", "Apple iPhone 15",
"Dell Laptop", "Canon Camera", "Nike Shoes", "Adidas T-Shirt" "Samsung Galaxy S23",
"Sony Headphones",
"Dell Laptop",
"Canon Camera",
"Nike Shoes",
"Adidas T-Shirt",
] ]
SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"] SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"]
SAMPLE_CONDITIONS = ["New", "Used", "Refurbished"] SAMPLE_CONDITIONS = ["New", "Used", "Refurbished"]
@ -116,7 +129,11 @@ class ProductForm(QDialog):
self, "Select Image Files", "", "Images (*.png *.jpg *.jpeg *.bmp)" self, "Select Image Files", "", "Images (*.png *.jpg *.jpeg *.bmp)"
) )
if files: 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)) self.images_input.setText(",".join(existing + files))
def save(self): def save(self):
@ -131,35 +148,61 @@ class ProductForm(QDialog):
QMessageBox.warning(self, "Error", "Price must be a valid number") QMessageBox.warning(self, "Error", "Price must be a valid number")
return return
category = self.category_input.currentText() if self.category_input.currentText() != "None" else None category = (
condition = self.condition_input.currentText() if self.condition_input.currentText() != "None" else None 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 brand = self.brand_input.text().strip() or None
description = self.description_input.toPlainText().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 sku = self.sku_input.text().strip() or None
location = self.location_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 = 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()] images = [img.strip() for img in raw_text.split(",") if img.strip()]
try: try:
if self.product and "id" in self.product: if self.product and "id" in self.product:
Product.update( Product.update(
self.product["id"], name, price, self.product["id"],
name,
price,
images=images, images=images,
url=None, status="draft", url=None,
category=category, condition=condition, brand=brand, status="draft",
description=description, tags=tags, sku=sku, location=location category=category,
condition=condition,
brand=brand,
description=description,
tags=tags,
sku=sku,
location=location,
) )
else: else:
Product.create( Product.create(
name, price, name,
price,
images=images, images=images,
url=None, status="draft", url=None,
category=category, condition=condition, brand=brand, status="draft",
description=description, tags=tags, sku=sku, location=location category=category,
condition=condition,
brand=brand,
description=description,
tags=tags,
sku=sku,
location=location,
) )
self.accept() self.accept()
except Exception as e: except Exception as e:

View File

@ -1,14 +1,27 @@
import json import json
from functools import partial from functools import partial
from services.core.loading_service import run_with_progress from services.core.loading_service import run_with_progress
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QWidget,
QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView, QVBoxLayout,
QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton, QStyle, QStylePainter, QLineEdit 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.product import Product
from database.models.listed import Listed
from services.image_service import ImageService from services.image_service import ImageService
from gui.tabs.products.forms.product_form import ProductForm from gui.tabs.products.forms.product_form import ProductForm
from gui.tabs.products.dialogs.product_filter_dialog import FilterDialog 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 PAGE_SIZE = 10
# --- Header Checkbox --- # --- Header Checkbox ---
class CheckBoxHeader(QHeaderView): class CheckBoxHeader(QHeaderView):
select_all_changed = pyqtSignal(bool) select_all_changed = pyqtSignal(bool)
@ -34,9 +48,14 @@ class CheckBoxHeader(QHeaderView):
x = rect.x() + (rect.width() - size) // 2 x = rect.x() + (rect.width() - size) // 2
y = rect.y() + (rect.height() - size) // 2 y = rect.y() + (rect.height() - size) // 2
option.rect = QRect(x, y, size, size) 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 = QStylePainter(self.viewport())
painter2.drawControl(QStyle.CE_CheckBox, option) painter2.drawControl(QStyle.ControlElement.CE_CheckBox, option)
def handle_section_pressed(self, logicalIndex): def handle_section_pressed(self, logicalIndex):
if logicalIndex == 0: if logicalIndex == 0:
@ -47,12 +66,7 @@ class CheckBoxHeader(QHeaderView):
# --- ProductTab --- # --- ProductTab ---
class ProductTab(QWidget): class ProductTab(QWidget):
SORTABLE_COLUMNS = { SORTABLE_COLUMNS = {1: "id", 3: "name", 4: "price", 7: "created_at"}
1: "id",
3: "name",
4: "price",
7: "created_at"
}
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -72,7 +86,9 @@ class ProductTab(QWidget):
top_layout.addWidget(self.add_btn) top_layout.addWidget(self.add_btn)
self.options_btn = QPushButton("Action") 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.setMinimumWidth(50)
self.options_btn.setMaximumWidth(120) self.options_btn.setMaximumWidth(120)
top_layout.addWidget(self.options_btn) top_layout.addWidget(self.options_btn)
@ -81,10 +97,10 @@ class ProductTab(QWidget):
# Table # Table
self.table = QTableWidget() self.table = QTableWidget()
self.table.verticalHeader().setDefaultSectionSize(60) self.table.verticalHeader().setDefaultSectionSize(60)
self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
header = CheckBoxHeader(Qt.Horizontal, self.table) header = CheckBoxHeader(Qt.Orientation.Horizontal, self.table)
self.table.setHorizontalHeader(header) self.table.setHorizontalHeader(header)
header.select_all_changed.connect(self.select_all_rows) header.select_all_changed.connect(self.select_all_rows)
header.sectionClicked.connect(self.handle_header_click) header.sectionClicked.connect(self.handle_header_click)
@ -143,14 +159,20 @@ class ProductTab(QWidget):
menu.addAction(action_clear) menu.addAction(action_clear)
# --- Thêm Add Listed Selected --- # --- Thêm Add Listed Selected ---
if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked() if any(
for i in range(self.table.rowCount())): 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 = QAction("Add Listed Selected", menu)
action_add_listed_selected.triggered.connect(self.add_listed_selected) action_add_listed_selected.triggered.connect(self.add_listed_selected)
menu.addAction(action_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() if any(
for i in range(self.table.rowCount())): 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 = QAction("Delete Selected", menu)
action_delete_selected.triggered.connect(self.delete_selected) action_delete_selected.triggered.connect(self.delete_selected)
menu.addAction(action_delete_selected) menu.addAction(action_delete_selected)
@ -160,7 +182,7 @@ class ProductTab(QWidget):
# --- Filter --- # --- Filter ---
def open_filter_dialog(self): def open_filter_dialog(self):
dialog = FilterDialog(self) dialog = FilterDialog(self)
if dialog.exec_(): if dialog.exec(): # PyQt6: exec()
self.filters = dialog.get_filters() self.filters = dialog.get_filters()
self.current_page = 0 self.current_page = 0
self.load_data() self.load_data()
@ -182,15 +204,28 @@ class ProductTab(QWidget):
offset = self.current_page * PAGE_SIZE offset = self.current_page * PAGE_SIZE
# Lấy toàn bộ dữ liệu cần load # Lấy toàn bộ dữ liệu cần load
page_items, total_count = Product.get_paginated( page_items, total_count = Product.get_paginated(
offset, PAGE_SIZE, self.filters, offset,
sort_by=self.sort_by, sort_order=self.sort_order PAGE_SIZE,
self.filters,
sort_by=self.sort_by,
sort_order=self.sort_order,
) )
self.total_count = total_count self.total_count = total_count
self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE) self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE)
self.table.setColumnCount(9) 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.setHorizontalHeaderLabels(columns)
self.table.setRowCount(len(page_items)) self.table.setRowCount(len(page_items))
@ -210,7 +245,7 @@ class ProductTab(QWidget):
if pixmap: if pixmap:
lbl = QLabel() lbl = QLabel()
lbl.setPixmap(pixmap) lbl.setPixmap(pixmap)
lbl.setAlignment(Qt.AlignCenter) lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.table.setCellWidget(i_row, 1, lbl) self.table.setCellWidget(i_row, 1, lbl)
else: else:
self.table.setItem(i_row, 1, QTableWidgetItem("None")) self.table.setItem(i_row, 1, QTableWidgetItem("None"))
@ -228,8 +263,11 @@ class ProductTab(QWidget):
created_ts = p.get("created_at") created_ts = p.get("created_at")
if created_ts: if created_ts:
from datetime import datetime from datetime import datetime
try: 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: except Exception:
created_str = str(created_ts) created_str = str(created_ts)
self.table.setItem(i_row, 7, QTableWidgetItem(created_str)) self.table.setItem(i_row, 7, QTableWidgetItem(created_str))
@ -258,7 +296,7 @@ class ProductTab(QWidget):
items_with_index, items_with_index,
handler=lambda x: handler(*x), handler=lambda x: handler(*x),
message="Loading products...", message="Loading products...",
parent=self parent=self,
) )
else: else:
for item in items_with_index: for item in items_with_index:
@ -266,18 +304,20 @@ class ProductTab(QWidget):
# Header sizing # Header sizing
header = self.table.horizontalHeader() header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.Fixed) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
self.table.setColumnWidth(1, 60) self.table.setColumnWidth(1, 60)
for idx in range(2, 8): for idx in range(2, 8):
header.setSectionResizeMode(idx, QHeaderView.Stretch) header.setSectionResizeMode(idx, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(8, QHeaderView.Fixed) header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed)
self.table.setColumnWidth(8, 100) self.table.setColumnWidth(8, 100)
# Pagination # Pagination
self.prev_btn.setEnabled(self.current_page > 0) self.prev_btn.setEnabled(self.current_page > 0)
self.next_btn.setEnabled(self.current_page < self.total_pages - 1) 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 # Reset header checkbox
if isinstance(header, CheckBoxHeader): if isinstance(header, CheckBoxHeader):
@ -286,8 +326,6 @@ class ProductTab(QWidget):
self.update_options_menu() self.update_options_menu()
# --- Go to page --- # --- Go to page ---
def go_to_page(self): def go_to_page(self):
try: try:
@ -310,70 +348,72 @@ class ProductTab(QWidget):
ids = [ ids = [
int(cb.property("product_id")) int(cb.property("product_id"))
for i in range(self.table.rowCount()) 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: if not ids:
QMessageBox.information(self, "Info", "No product selected") QMessageBox.information(self, "Info", "No product selected")
return return
confirm = QMessageBox.question( confirm = QMessageBox.question(
self, "Confirm Delete", f"Delete {len(ids)} selected products?", self,
QMessageBox.Yes | QMessageBox.No "Confirm Delete",
f"Delete {len(ids)} selected products?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
) )
if confirm != QMessageBox.Yes: if confirm != QMessageBox.StandardButton.Yes:
return return
# --- dùng run_with_progress --- # --- dùng run_with_progress ---
run_with_progress( run_with_progress(
ids, ids, handler=Product.delete, message="Deleting products...", parent=self
handler=Product.delete,
message="Deleting products...",
parent=self
) )
self.current_page = 0 self.current_page = 0
self.load_data() self.load_data()
# --- Product Actions --- # --- Product Actions ---
def add_product(self): def add_product(self):
form = ProductForm(self) form = ProductForm(self)
if form.exec_(): if form.exec(): # PyQt6: exec()
self.current_page = 0 self.current_page = 0
self.load_data() self.load_data()
def edit_product(self, product): def edit_product(self, product):
form = ProductForm(self, product) form = ProductForm(self, product)
if form.exec_(): if form.exec(): # PyQt6: exec()
self.load_data() self.load_data()
def delete_product(self, product_id): def delete_product(self, product_id):
confirm = QMessageBox.question( 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 return
run_with_progress( run_with_progress(
[product_id], [product_id],
handler=Product.delete, handler=Product.delete,
message="Deleting product...", message="Deleting product...",
parent=self parent=self,
) )
self.load_data() self.load_data()
def add_listed_selected(self): def add_listed_selected(self):
selected_ids = [ selected_ids = [
int(cb.property("product_id")) int(cb.property("product_id"))
for i in range(self.table.rowCount()) 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: if not selected_ids:
QMessageBox.information(self, "Info", "No products selected") QMessageBox.information(self, "Info", "No products selected")
return return
dialog = AddListedDialog(selected_ids, parent=self) dialog = AddListedDialog(selected_ids, parent=self)
dialog.exec_() dialog.exec() # PyQt6
# --- Clear row checkboxes --- # --- Clear row checkboxes ---
for i in range(self.table.rowCount()): for i in range(self.table.rowCount()):
@ -387,7 +427,6 @@ class ProductTab(QWidget):
# --- Update menu --- # --- Update menu ---
self.update_options_menu() self.update_options_menu()
def reset_header_checkbox(self): def reset_header_checkbox(self):
header = self.table.horizontalHeader() header = self.table.horizontalHeader()
if isinstance(header, CheckBoxHeader): if isinstance(header, CheckBoxHeader):
@ -401,8 +440,7 @@ class ProductTab(QWidget):
return return
dialog = AddListedDialog([product_id], parent=self) dialog = AddListedDialog([product_id], parent=self)
dialog.exec_() dialog.exec() # PyQt6
# --- Pagination --- # --- Pagination ---
def next_page(self): def next_page(self):

View File

@ -1,9 +1,15 @@
from PyQt5.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QLineEdit, QDialog,
QComboBox, QDialogButtonBox, QMessageBox QVBoxLayout,
QLabel,
QLineEdit,
QComboBox,
QDialogButtonBox,
QMessageBox,
) )
from PyQt5.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt5.QtGui import QIntValidator from PyQt6.QtGui import QIntValidator
class SettingForm(QDialog): class SettingForm(QDialog):
def __init__(self, key: str, value: str, type_: str = "text", parent=None): 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"): if self.type_ == "boolean" or val_lower in ("true", "false"):
self.input_widget = QComboBox() self.input_widget = QComboBox()
self.input_widget.addItems(["true", "false"]) 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: else:
self.input_widget = QLineEdit() self.input_widget = QLineEdit()
self.input_widget.setText(value or "") self.input_widget.setText(value or "")
@ -38,18 +46,23 @@ class SettingForm(QDialog):
self.layout.addWidget(self.input_widget) self.layout.addWidget(self.input_widget)
# --- Buttons --- # --- Buttons ---
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.on_accept) buttons.accepted.connect(self.on_accept)
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
self.layout.addWidget(buttons) self.layout.addWidget(buttons)
# ----------------------------------------------------------------------
def on_accept(self): def on_accept(self):
"""Xử lý khi người dùng nhấn OK"""
if isinstance(self.input_widget, QLineEdit): if isinstance(self.input_widget, QLineEdit):
val = self.input_widget.text() val = self.input_widget.text()
if self.type_ == "number": if self.type_ == "number" and not val.isdigit():
if not val.isdigit(): QMessageBox.warning(
QMessageBox.warning(self, "Invalid input", "Please enter a valid number.") self, "Invalid input", "Please enter a valid number."
return )
return
elif isinstance(self.input_widget, QComboBox): elif isinstance(self.input_widget, QComboBox):
val = self.input_widget.currentText() val = self.input_widget.currentText()
else: else:
@ -62,10 +75,12 @@ class SettingForm(QDialog):
self.new_value = val self.new_value = val
self.accept() self.accept()
# ----------------------------------------------------------------------
@staticmethod @staticmethod
def get_new_value(key: str, value: str, type_: str = "text", parent=None): 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) dialog = SettingForm(key, value, type_=type_, parent=parent)
result = dialog.exec_() result = dialog.exec() # ✅ PyQt6 dùng exec() thay vì exec_()
if result == QDialog.Accepted: if result == QDialog.DialogCode.Accepted:
return dialog.new_value return dialog.new_value
return None return None

View File

@ -1,18 +1,25 @@
from functools import partial from functools import partial
import json from PyQt6.QtWidgets import (
from PyQt5.QtWidgets import ( QWidget,
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QMessageBox, QMenu, QTableWidget,
QAction, QHeaderView, QSizePolicy, QLineEdit 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 services.core.loading_service import run_with_progress
from database.models.setting import Setting from database.models.setting import Setting
from .forms.setting_form import SettingForm from .forms.setting_form import SettingForm
PAGE_SIZE = 10 PAGE_SIZE = 10
# --- Settings Tab ---
class SettingsTab(QWidget): class SettingsTab(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -24,18 +31,18 @@ class SettingsTab(QWidget):
layout = QVBoxLayout() layout = QVBoxLayout()
# Top Layout # --- Top layout (placeholder cho search/filter nếu cần) ---
top_layout = QHBoxLayout() top_layout = QHBoxLayout()
top_layout.addStretch() top_layout.addStretch()
layout.addLayout(top_layout) layout.addLayout(top_layout)
# Table # --- Table ---
self.table = QTableWidget() self.table = QTableWidget()
self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
layout.addWidget(self.table) layout.addWidget(self.table)
# Pagination # --- Pagination ---
pag_layout = QHBoxLayout() pag_layout = QHBoxLayout()
self.prev_btn = QPushButton("Previous") self.prev_btn = QPushButton("Previous")
self.prev_btn.clicked.connect(self.prev_page) self.prev_btn.clicked.connect(self.prev_page)
@ -57,8 +64,9 @@ class SettingsTab(QWidget):
self.setLayout(layout) self.setLayout(layout)
# --- Load Data --- # ----------------------------------------------------------------------
def load_data(self, show_progress=True): def load_data(self, show_progress=True):
"""Nạp dữ liệu setting vào bảng"""
self.table.clearContents() self.table.clearContents()
self.table.setRowCount(0) self.table.setRowCount(0)
@ -82,12 +90,12 @@ class SettingsTab(QWidget):
# ID # ID
id_item = QTableWidgetItem(str(setting_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) self.table.setItem(i_row, 0, id_item)
# Key # Key
key_item = QTableWidgetItem(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) self.table.setItem(i_row, 1, key_item)
# Value # Value
@ -99,32 +107,42 @@ class SettingsTab(QWidget):
btn_menu.setMaximumWidth(120) btn_menu.setMaximumWidth(120)
menu = QMenu(btn_menu) menu = QMenu(btn_menu)
act_edit = menu.addAction("Edit") 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) btn_menu.setMenu(menu)
self.table.setCellWidget(i_row, 3, btn_menu) self.table.setCellWidget(i_row, 3, btn_menu)
items_with_index = [(s, i) for i, s in enumerate(settings)] items_with_index = [(s, i) for i, s in enumerate(settings)]
if show_progress: 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: else:
for item in items_with_index: for item in items_with_index:
handler(*item) handler(*item)
# Resize header # --- Resize header ---
header = self.table.horizontalHeader() header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # ID
header.setSectionResizeMode(1, QHeaderView.Stretch) # Key header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Key
header.setSectionResizeMode(2, QHeaderView.Stretch) # Value header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Value
header.setSectionResizeMode(3, QHeaderView.Fixed) # Action header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Action
self.table.setColumnWidth(3, 120) self.table.setColumnWidth(3, 120)
# Pagination # --- Pagination ---
self.prev_btn.setEnabled(self.current_page > 0) self.prev_btn.setEnabled(self.current_page > 0)
self.next_btn.setEnabled(self.current_page < self.total_pages - 1) 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_): def edit_setting(self, setting_id, key, row, type_):
"""Chỉnh sửa giá trị setting"""
old_value = self.table.item(row, 2).text() old_value = self.table.item(row, 2).text()
new_value = SettingForm.get_new_value(key, old_value, type_=type_, parent=self) new_value = SettingForm.get_new_value(key, old_value, type_=type_, parent=self)
if new_value is not None and new_value != old_value: if new_value is not None and new_value != old_value:
@ -134,8 +152,9 @@ class SettingsTab(QWidget):
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to update setting: {e}") QMessageBox.critical(self, "Error", f"Failed to update setting: {e}")
# --- Pagination --- # ----------------------------------------------------------------------
def go_to_page(self): def go_to_page(self):
"""Nhảy đến trang cụ thể"""
try: try:
page = int(self.page_input.text()) - 1 page = int(self.page_input.text()) - 1
if 0 <= page < self.total_pages: if 0 <= page < self.total_pages:
@ -145,11 +164,13 @@ class SettingsTab(QWidget):
pass pass
def next_page(self): def next_page(self):
"""Trang kế"""
if self.current_page < self.total_pages - 1: if self.current_page < self.total_pages - 1:
self.current_page += 1 self.current_page += 1
self.load_data() self.load_data()
def prev_page(self): def prev_page(self):
"""Trang trước"""
if self.current_page > 0: if self.current_page > 0:
self.current_page -= 1 self.current_page -= 1
self.load_data() self.load_data()

View File

@ -1,7 +1,6 @@
PyQt5==5.15.11 PyQt6==6.7.0
PyQtWebEngine==5.15.6 PyQt6-WebEngine==6.7.0
opencv-python==4.10.0.84 opencv-python==4.10.0.84
numpy==1.26.4 numpy==1.26.4
SQLAlchemy==2.0.22 SQLAlchemy==2.0.22
requests requests

View File

@ -1,4 +1,4 @@
from PyQt5.QtCore import Qt, QTimer from PyQt6.QtCore import Qt, QTimer
class ActionService: class ActionService:
@ -16,7 +16,7 @@ class ActionService:
self.delay = delay self.delay = delay
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
def _run_js(self, script): def _run_js(self, script: str):
"""Chạy JavaScript trên webview""" """Chạy JavaScript trên webview"""
if not self.webview: if not self.webview:
print("[WARN] Không có webview để chạy JS.") print("[WARN] Không có webview để chạy JS.")

View File

@ -1,7 +1,10 @@
from PyQt5.QtWidgets import QProgressDialog from PyQt6.QtWidgets import QProgressDialog
from PyQt5.QtCore import Qt, QCoreApplication 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. Run the handler for each item in `items` and display a loading progress bar.
- items: iterable (list, tuple, ...) - items: iterable (list, tuple, ...)
@ -15,7 +18,7 @@ def run_with_progress(items, handler, message="Loading...", cancel_text="Cancel"
return 0, 0 return 0, 0
progress = QProgressDialog(message, cancel_text, 0, total, parent) 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.setMinimumDuration(0)
progress.setValue(0) progress.setValue(0)
@ -33,7 +36,7 @@ def run_with_progress(items, handler, message="Loading...", cancel_text="Cancel"
fail_count += 1 fail_count += 1
progress.setValue(i + 1) progress.setValue(i + 1)
QCoreApplication.processEvents() # Prevent UI freezing QCoreApplication.processEvents() # Giữ UI không bị treo
progress.close() progress.close()
return success_count, fail_count return success_count, fail_count

View File

@ -1,8 +1,9 @@
# services/image_service.py # services/image_service.py
import os import os
import requests import requests
from PyQt5.QtGui import QPixmap from PyQt6.QtGui import QPixmap
from PyQt5.QtCore import Qt from PyQt6.QtCore import Qt
class ImageService: class ImageService:
@staticmethod @staticmethod
@ -35,7 +36,13 @@ class ImageService:
return None return None
if not pixmap.isNull(): 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 pixmap
return None return None

View File

@ -62,7 +62,9 @@ class ProfileService:
"""Check folder có tồn tại không""" """Check folder có tồn tại không"""
return os.path.isdir(self.get_profile_path(name)) 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. Tạo folder profile.
- name: email/username - name: email/username
@ -118,19 +120,21 @@ class ProfileService:
return [] return []
# ----------------- Optional: QWebEngineProfile creator ----------------- # ----------------- 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 trả về QWebEngineProfile đã cấu hình persistent storage (cookies, local storage, cache). Tạo 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 - name: email/username để đặt thư mục lưu
- parent: parent QObject cho QWebEngineProfile (thường self) - parent: parent QObject cho QWebEngineProfile (thường self)
- profile_id: tên id cho profile (tùy chọn) - profile_id: tên id cho profile (tùy chọn)
""" """
try: try:
from PyQt5.QtWebEngineWidgets import QWebEngineProfile from PyQt6.QtWebEngineWidgets import QWebEngineProfile
except Exception as e: except Exception as e:
raise RuntimeError( raise RuntimeError(
"PyQt5.QtWebEngineWidgets không khả dụng. " "PyQt6.QtWebEngineWidgets không khả dụng. "
"Không thể tạo QWebEngineProfile." "Không thể tạo QWebEngineProfile."
) from e ) from e
@ -143,8 +147,13 @@ class ProfileService:
profile.setCachePath(profile_path) profile.setCachePath(profile_path)
# Force lưu cookie persist # Force lưu cookie persist
try: try:
profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies) from PyQt6.QtWebEngineCore import QWebEngineProfile as CoreProfile
profile.setPersistentCookiesPolicy(
CoreProfile.PersistentCookiesPolicy.ForcePersistentCookies
)
except Exception: except Exception:
pass pass
logger.info("Created QWebEngineProfile for %s -> %s", name, profile_path) logger.info("Created QWebEngineProfile for %s -> %s", name, profile_path)
return profile return profile

View File

@ -1,6 +1,7 @@
from PyQt5.QtCore import QObject from PyQt6.QtCore import QObject
import threading import threading
class SharedStore(QObject): class SharedStore(QObject):
_instance = None _instance = None
_lock = threading.Lock() _lock = threading.Lock()
@ -24,7 +25,7 @@ class SharedStore(QObject):
def remove(self, listed_id: int): def remove(self, listed_id: int):
with self._items_lock: 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: def size(self) -> int:
with self._items_lock: with self._items_lock: