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
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.

View File

@ -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())

View File

@ -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}"
)

View File

@ -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()

View File

@ -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())

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.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()

View File

@ -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()

View File

@ -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}")

View File

@ -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

View File

@ -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)

View File

@ -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()

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 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()

View File

@ -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)

View File

@ -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:

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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.")

View File

@ -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

View File

@ -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

View File

@ -35,7 +35,7 @@ class ProfileService:
Service để quản 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 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 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

View File

@ -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: